Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .claude/skills/setup-agent-team/discovery.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -201,12 +201,25 @@ For each candidate, verify:
- Has affordable CPU instances (pay-per-hour or pay-per-second pricing)
- Is actually available (not waitlisted/invite-only)

**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update:
1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording
2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133)
3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format

Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely.
**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files:

**`test/record.sh`** (fixture recording):
1. Add the cloud to `ALL_RECORDABLE_CLOUDS`
2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints
3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var
4. Add a case in `call_api()` to dispatch to the cloud's API function
5. Add a case in `has_api_error()` for the cloud's error response format
6. Add a `_live_{cloud}()` function for create/delete server fixture recording

**`test/mock.sh`** (mock test infrastructure — ALL of these):
1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings.
2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`)
3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint)
4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.)
5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()`
6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()`

Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely.

### Agent Scout (spawn 1, only if justified)
Research new AI agents, BUT only add one if there's REAL community demand:
Expand DownExpand Up@@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with
REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds.
Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest.

MANDATORY: When adding a new cloud, also add it to the test infrastructure:
MANDATORY: When adding a new cloud, also update the test infrastructure:
1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function
2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block)
2. test/mock.sh — add ALL of these:
- _strip_api_base(): URL-stripping case for the cloud's API base URL
- _validate_body(): body validation case with server creation endpoint + required fields
- assert_cloud_api_calls(): expected API calls (SSH key fetch + server create)
- setup_env_for_cloud(): test env vars (API token, server name, plan, region)

Only add a new AGENT if you find one with real community buzz:
- 1000+ GitHub stars
Expand Down
147 changes: 128 additions & 19 deletions .claude/skills/setup-agent-team/qa-cycle.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do
fi
done

# Close stale qa PRs (open > 2 hours)
# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review
STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \
--jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true
for pr_num in $STALE_PRS; do
if [[ -n "$pr_num" ]]; then
PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN"
if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then
log "Merging stale QA PR #${pr_num}..."
gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true
else
log "Closing unmergeable stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.
log "Closing stale QA PR #${pr_num}..."
gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle.

-- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true
fi
fi
done

Expand DownExpand Up@@ -228,8 +222,14 @@ check_timeout || exit 0
log "=== Phase 1: Record fixtures ==="

RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt"
RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json"
rm -f "${RECORD_OUTPUT}"

# Initialize failure tracker if missing
if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then
printf '{}' > "${RECORD_FAILURES_FILE}"
fi

RECORD_EXIT=0
bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$?

Expand DownExpand Up@@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru
fi
fi

# --- Track consecutive Phase 1 failures per cloud ---
# Parse the final recording output to determine which clouds failed
FINAL_RECORD_FAILED=""
FINAL_RECORD_SUCCEEDED=""
if [[ -f "${RECORD_OUTPUT}" ]]; then
_current_cloud=""
_cloud_had_error=""
while IFS= read -r line; do
clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g')
case "$clean" in
*"Recording "*" ━━━"*)
# Save previous cloud result
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
_current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//')
_cloud_had_error=""
;;
*"fail "*)
_cloud_had_error="true"
;;
*"done "*)
# Cloud section ended
;;
esac
done < "${RECORD_OUTPUT}"
# Handle last cloud
if [[ -n "${_current_cloud}" ]]; then
if [[ "${_cloud_had_error}" == "true" ]]; then
FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}"
else
FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}"
fi
fi
fi
FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //')
FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //')

# Update the persistent failure tracker
if [[ -f "${RECORD_FAILURES_FILE}" ]]; then
python3 -c "
import json, sys

tracker_path = sys.argv[1]
failed = sys.argv[2].split() if sys.argv[2] else []
succeeded = sys.argv[3].split() if sys.argv[3] else []

try:
with open(tracker_path) as f:
tracker = json.load(f)
except (json.JSONDecodeError, FileNotFoundError):
tracker = {}

# Increment consecutive failures for failed clouds
for cloud in failed:
tracker[cloud] = tracker.get(cloud, 0) + 1

# Reset counter for clouds that succeeded
for cloud in succeeded:
tracker[cloud] = 0

with open(tracker_path, 'w') as f:
json.dump(tracker, f, indent=2, sort_keys=True)

# Output clouds that hit the threshold (3+ consecutive failures)
escalate = [c for c, count in tracker.items() if count >= 3]
if escalate:
print(' '.join(escalate))
" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true

ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true)
rm -f /tmp/spawn-qa-escalate.txt

if [[ -n "${ESCALATE_CLOUDS}" ]]; then
for cloud in ${ESCALATE_CLOUDS}; do
consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+")
log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles"

# Check if an issue already exists for this cloud
existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \
--search "fixture recording failing ${cloud}" \
--json number --jq '.[0].number' 2>/dev/null) || existing_issue=""

if [[ -z "${existing_issue}" ]]; then
gh issue create --repo OpenRouterTeam/spawn \
--title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \
--body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \
--label "bug" \
2>&1 | tee -a "${LOG_FILE}" || true
log "Phase 1: Created GitHub issue for ${cloud} persistent failure"
else
log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate"
fi
done
fi
fi

rm -f "${RECORD_OUTPUT}"
check_timeout || exit 0

Expand DownExpand Up@@ -448,6 +549,15 @@ else
FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true)
fi

# Capture full mock test output per-cloud for richer agent context
MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output"
rm -rf "${MOCK_OUTPUT_DIR}"
mkdir -p "${MOCK_OUTPUT_DIR}"
for cloud in $FAILED_CLOUDS; do
log "Phase 3: Capturing full mock test output for ${cloud}..."
bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true
done

AGENT_PIDS=""
for cloud in $FAILED_CLOUDS; do
check_timeout || break
Expand All@@ -456,25 +566,21 @@ else
cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true)
failing_scripts=""
failing_agents=""
error_context=""
for combo in $cloud_failures; do
agent=$(printf '%s' "$combo" | cut -d/ -f2)
script_path="${cloud}/${agent}.sh"
failing_scripts="${failing_scripts} ${script_path}"
failing_agents="${failing_agents} ${agent}"
if [[ -f "${LOG_FILE}" ]]; then
ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true)
if [[ -n "$ctx" ]]; then
error_context="${error_context}
--- ${script_path} ---
${ctx}
"
fi
fi
done
failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //')
failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //')

# Use full mock test output as error context (not just 10 lines)
error_context=""
if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then
error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log")
fi

fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ')
log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}"

Expand DownExpand Up@@ -582,6 +688,9 @@ FIXEOF
done
git worktree prune 2>/dev/null || true

# Clean up per-cloud mock output
rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true

log "Phase 3: Fix agents complete"
fi

Expand Down
1 change: 1 addition & 0 deletions .claude/skills/setup-trigger-service/qa-cycle.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these artifacts

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
name: Test

on:
pull_request:
branches: [main]

concurrency:
group: test-${{ github.head_ref }}
cancel-in-progress: true

jobs:
mock-tests:
name: Mock Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run mock tests
run: |
bash test/mock.sh 2>&1 | tee /tmp/mock-output.log
- name: Post summary
if: always()
run: |
echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ node_modules/
.docs/
__pycache__/
.claude/skills/*/start-*.sh
.claude/settings.local.json
cli/cli.js

# Sensitive files — never commit secrets or private keys
Expand All@@ -15,3 +16,5 @@ id_rsa
id_ed25519
credentials.json
service-account.json
.claude/settings.json
.DS_Store
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it:
- Run tests with `bun test`
- Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"`

### Mock Test Infrastructure (MANDATORY for new clouds)

When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`:

| Function | What to add |
|---|---|
| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) |
| `_validate_body()` | Server creation endpoint + required POST fields |
| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) |
| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) |

And in `test/record.sh`:

| Function | What to add |
|---|---|
| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list |
| `get_endpoints()` | GET-only API endpoints for fixture recording |
| `get_auth_env_var()` | Auth env var name mapping |
| `call_api()` | API function dispatcher |
| `has_api_error()` | Error response detection for the cloud's API |

Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds.

## CLI Version Management

**CRITICAL: Bump the version on every CLI change!**
Expand Down
8 changes: 4 additions & 4 deletions genesiscloud/claude.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,9 +20,9 @@ create_server "${SERVER_NAME}"
verify_server_connectivity "${GENESIS_SERVER_IP}"
wait_for_cloud_init "${GENESIS_SERVER_IP}" 60

log_step "Verifying Claude Code installation..."
log_warn "Verifying Claude Code installation..."
if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_step "Claude Code not found, installing manually..."
log_warn "Claude Code not found, installing manually..."
run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
log_info "Claude Code is installed"
Expand All@@ -34,7 +34,7 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi

log_step "Setting up environment variables..."
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
Expand All@@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})"
echo ""

log_step "Starting Claude Code..."
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
Loading