Skill: z-team-alpha/overnight-factory (version 1.0.0)

--- name: overnight-factory description: | Set up an AI agent as an Overnight Software Factory operator. Use when configuring an OpenClaw agent to autonomously handle support tickets end-to-end: receive ticket assignments via email or GitHub, analyze issues, spawn Claude Code to implement fixes, open PRs, and notify a human for review. Triggers on phrases like "set up overnight factory", "configure ticket pipeline", "automate support tickets", "set up AI coding agent for tickets", or "overnight software factory". --- # Overnight Factory Configure this agent as an autonomous ticket-to-PR operator. Human reviews PRs; agent handles everything else. ## Architecture ``` TinyDesk/support system → GitHub issue (assigned to bot account) → Email notification to agent inbox ↓ Cron job (every 15min, isolated session) ├─ Email check: detect assignment emails ├─ GitHub poll: catch missed emails (safety net) └─ Dispatch: spawn subagent per ticket ↓ Subagent (Claude Code) ├─ Analyze issue + screenshots ├─ Explore codebase ├─ Post analysis comment on issue ├─ Implement fix (branch → commit → push) └─ Open PR → notify human ``` ## Setup Steps ### 1. Identity & Credentials Create workspace files: `IDENTITY.md`, `USER.md` (who you're helping), `SOUL.md`. Add `.env` to workspace: ``` GITHUB_TOKEN=ghp_... EMAIL_USER=[REDACTED_SECRET] EMAIL_PASSWORD=... ``` Configure git: ```bash git config --global user.name "Your Bot Name" git config --global user.email "[REDACTED_SECRET]" echo "[REDACTED_SECRET]" > ~/.git-credentials git config --global credential.helper store ``` ### 2. Verify Claude Code ```bash /path/to/claude --version /path/to/claude -p --dangerously-skip-permissions --output-format text "echo hello" ``` Note the exact path — you'll need it. ### 3. Create the Cron Job ```bash openclaw cron add \ --name "email-check" \ --every 15m \ --session isolated \ --announce \ --to <YOUR-TELEGRAM-CHAT-ID> \ # explicit ID, NOT --channel last --timeout-seconds 120 \ --description "Check email + GitHub for ticket assignments" \ --message "$(cat /path/to/cron-prompt.txt)" ``` See `references/cron-prompt.md` for the full prompt template. ### 4. Keep HEARTBEAT.md Lightweight The cron handles email. Heartbeat should only contain tasks that need main-session context (e.g., PR monitoring). ## Critical Rules **Cron delivery:** Always use `--to <explicit-chat-id>`. Never `--channel last` — it doesn't resolve in isolated sessions. **Cron timeout:** Keep the prompt fast. Check email → spawn subagents → log → exit. Never wait for subagents inline. 120s is enough. **Error backoff:** After 5+ consecutive errors the cron backs off exponentially. Delete and recreate to reset: `openclaw cron delete <id> && openclaw cron add ...` **Fire-and-forget:** Subagents don't reliably call back to Telegram. Have the cron itself announce dispatches in its own reply (the cron reply is what gets delivered). ## Validation Checklist Before going live: - [ ] Send test email to support address — cron picks it up within 15 min - [ ] Cron dispatch reply arrives on Telegram - [ ] Subagent opens a real branch + PR - [ ] Assign a GitHub issue to bot manually — GitHub poll catches it - [ ] `lastDelivered: false` on empty runs is expected (HEARTBEAT_OK is silent by design) ## Debugging ```bash # Check cron status openclaw cron list --json | jq '.jobs[0].state' # Key fields to check: # lastRunStatus: "ok" or "error" # consecutiveErrors: should be 0 # lastDelivered: false on empty runs is normal; false on ticket-dispatch runs is a bug # lastDeliveryStatus: "not-delivered" = delivery config wrong # Check recent logs tail -20 memory/heartbeat-log.jsonl # Check inbox directly python3 -c " import imaplib, ssl ctx = ssl.create_default_context() M = imaplib.IMAP4_SSL('imap.ionos.com', 993, ssl_context=ctx) M.login('[REDACTED_SECRET]', 'password') M.select('INBOX') print('Unread:', len(M.search(None, 'UNSEEN')[1][0].split())) M.logout() " ``` ## Reference Files - **`references/cron-prompt.md`** — full cron job prompt template with email check + GitHub poll - **`references/ticket-pipeline.md`** — how to instruct subagents to handle tickets end-to-end - **`references/lessons-learned.md`** — real failure modes and fixes from production use

path: references/cron-prompt.md
content: # Cron Job Prompt Template

Use this as the `--message` for the `email-check` cron job. Customize the placeholders.

```
You are <AGENT_NAME>. Check support email AND poll GitHub for missed ticket assignments. Be fast — check, spawn work, log, exit.

## Step 1: Check email
Connect to <IMAP_HOST>:993 as <EMAIL_USER> ([REDACTED_SECRET] from workspace .env).
Fetch all UNSEEN emails. Mark them all read immediately.
For each ticket-assignment email from the bot account (subject contains "assigned"): extract the GitHub issue URL and add to a "to_process" list.

## Step 2: Poll GitHub for missed assignments (safety net)
Using GITHUB_TOKEN from workspace .env, call:
  GET https://api.github.com/search/issues?q=assignee:<BOT_GITHUB_USERNAME>+state:open+is:issue&per_page=50

For each result, check if already actioned (ALL must be true to skip):
- Present in memory/support-tickets.json (matched by issue_url)?
- Does <BOT_GITHUB_USERNAME> already have a comment on the issue?
- Is there an open or closed PR referencing the issue?

If NOT actioned → add to "to_process" list, mark as "recovered via GitHub poll" in log.

## Step 3: For each issue in "to_process"
- Fetch the full issue via GitHub API
- Spawn ONE subagent (runtime=subagent, runTimeoutSeconds=1800) per ticket with instructions to:
  - Analyze screenshots (image tool on any URLs in issue body)
  - Explore the relevant repo codebase
  - Post a detailed analysis comment on the issue
  - Create branch, implement fix (trivial commit if test ticket, real fix + tests otherwise), push, open PR
  - After opening PR: use sessions_send targeting label "main" to message <HUMAN_NAME> the PR URL
- Add issue to memory/support-tickets.json with status "dispatched"

## Step 4: In YOUR reply, list what was found:
- "📧 Email: N tickets found: <TICKET-IDs>"
- "🔍 GitHub poll: N missed tickets recovered: <issue URLs>"
- "🎫 Dispatched: <TICKET-ID> — <issue URL>"
If nothing found anywhere: reply HEARTBEAT_OK

## Step 5: Log
Append to memory/heartbeat-log.jsonl:
{"ts": "<ISO>", "emails_checked": N, "github_poll_checked": N, "recovered": N, "tickets_dispatched": N, "summary": "..."}
```

## Placeholders to Replace

| Placeholder | Example |
|---|---|
| `<AGENT_NAME>` | `alpha-2` |
| `<IMAP_HOST>` | `imap.ionos.com` |
| `<EMAIL_USER>` | `[REDACTED_SECRET]` |
| `<BOT_GITHUB_USERNAME>` | `z-team-alpha` |
| `<HUMAN_NAME>` | `Sze` |

## Cron Add Command

```bash
openclaw cron add \
  --name "email-check" \
  --every 15m \
  --session isolated \
  --announce \
  --to <TELEGRAM_CHAT_ID> \
  --timeout-seconds 120 \
  --description "Check email + GitHub for ticket assignments" \
  --message "$(cat /path/to/cron-prompt.txt)"
```

**Never use `--channel last`** — it doesn't resolve in isolated sessions.


path: references/lessons-learned.md
content: # Lessons Learned in Production

Real failure modes encountered and their fixes.

## Cron Delivery

**Problem:** Cron runs successfully but notifications never reach Telegram.
**Symptom:** `lastDelivered: false`, `lastDeliveryStatus: "not-delivered"` on every run.
**Cause:** `--channel last` doesn't resolve in isolated sessions — no "last" channel exists.
**Fix:** Use `--to <explicit-telegram-chat-id>` when creating/editing the cron.
**Note:** `lastDelivered: false` on empty-inbox runs is EXPECTED (HEARTBEAT_OK is intentionally silent).

## Cron Timeouts

**Problem:** Cron errors out on every run with `consecutiveErrors: 5+`.
**Symptom:** `lastError: "cron: job execution timed out"`, `lastDurationMs` equals your timeout exactly.
**Cause:** Cron prompt instructs it to wait for subagents to complete, which takes 10-30+ minutes.
**Fix:** Fire-and-forget design — spawn subagents without waiting, exit immediately. 120s timeout is sufficient.

## Error Backoff

**Problem:** After fixing a cron issue, it doesn't run for 30-60 minutes.
**Cause:** Exponential backoff after consecutive errors. Edit doesn't reset error count.
**Fix:** Delete and recreate: `openclaw cron delete <id> && openclaw cron add ...`

## Subagent Notification Failure

**Problem:** Subagent completes and opens PR, but no Telegram notification arrives.
**Cause:** Subagents spawned from isolated cron sessions can't reliably use sessions_send to reach the main session.
**Fix:** Have the cron itself announce dispatches in its own reply (before spawning). The cron reply is what reliably delivers to Telegram. Subagent notification is best-effort bonus.

## Missed Tickets (Email Loss)

**Problem:** TinyDesk creates the GitHub issue and assigns it, but the assignment email is never received.
**Cause:** Email delivery is a single point of failure.
**Fix:** Add GitHub poll as safety net in the cron — `GET /search/issues?q=assignee:<bot>+state:open` every 15min. Cross-check against support-tickets.json + existing comments + existing PRs to avoid re-dispatching.

## ACP Spawn Failures

**Problem:** `sessions_spawn` with `runtime: "acp"` fails with "unknown option '--format'".
**Cause:** acpx plugin passes `--format` flag; older Claude Code versions use `--output-format`.
**Fix:** Spawn a subagent (`runtime: "subagent"`) that runs `claude -p --dangerously-skip-permissions --output-format text "..."` via exec instead of using ACP directly.

## Heartbeat vs Cron Confusion

**Problem:** Email checks appear to work (logs show activity) but tickets are missed.
**Cause:** Heartbeat (main session) was doing email checks sporadically; the isolated cron job was either broken or never running.
**Diagnosis:** Check `openclaw cron list --json` — if `lastRunAtMs: null` or `consecutiveErrors > 0`, the cron is the problem.
**Fix:** Separate concerns cleanly: heartbeat handles main-session tasks only, cron handles email + GitHub polling.

## Duplicate Dispatch

**Problem:** Same ticket dispatched multiple times.
**Cause:** GitHub poll marks a ticket as unactioned if the comment or PR check doesn't match exactly.
**Fix:** Add `issue_url` to support-tickets.json immediately on dispatch (before subagent runs). Check this file first in the GitHub poll before checking comments/PRs.


path: references/ticket-pipeline.md
content: # Ticket Pipeline: Subagent Instructions

When dispatching a subagent to handle a ticket, include these instructions:

## Subagent Task Template

```
You are handling ticket <TICKET-ID> for <AGENT_NAME>.

**Issue:** <GITHUB_ISSUE_URL>
**Repo:** <LOCAL_REPO_PATH> (clone if missing)
**GitHub token:** Read GITHUB_TOKEN from workspace .env
**Git identity:** <BOT_NAME> <<BOT_EMAIL>>

## Step 1: Understand the ticket
- Fetch the full issue from GitHub API
- If there are screenshot URLs in the issue body, analyze them with the image tool
- Read the relevant files in the codebase to understand the context

## Step 2: Post analysis comment
Post a comment on the GitHub issue with:
- What the bug/feature is (from screenshots + description)
- Which files/components are affected
- Your proposed approach

## Step 3: Implement
- For TEST tickets (body contains "test" or no real bug described): make a trivial commit (add a comment line to a relevant file)
- For real tickets: implement the fix with tests
- Branch name: fix/<ticket-id>-<short-description> or feat/<ticket-id>-<short-description>
- Commit message: "fix: <description> (<TICKET-ID>)" or "feat: ..."

## Step 4: Open PR
- Push branch
- Open PR via GitHub API referencing the issue
- PR title: "<type>: <description> (<TICKET-ID>)"
- PR body: "Closes #<issue-number>\n\n<brief description of changes>"

## Step 5: Notify
- Write PR URL to memory/<ticket-id>-pr.txt
- Use sessions_send tool targeting label "main" with: "🏭 <TICKET-ID> PR ready: <PR_URL> — <one-line summary>"
- Output: PR_URL=<url>
```

## Repo Management

Clone repos on demand, keep them in `workspace/repos/<repo-name>`:

```bash
cd workspace/repos
git clone [REDACTED_SECRET]
cd <REPO>
git config user.name "<BOT_NAME>"
git config user.email "<BOT_EMAIL>"
git remote set-url origin [REDACTED_SECRET]
```

## State Tracking

Update `memory/support-tickets.json` after each dispatch:

```json
[
  {
    "ticket_id": "TD-0015",
    "email_from": "[REDACTED_SECRET]",
    "issue_url": "https://github.com/org/repo/issues/2",
    "pr_url": "https://github.com/org/repo/pull/3",
    "status": "pr_open",
    "created_at": "2026-03-14T23:00:00Z"
  }
]
```

Status values: `dispatched` → `pr_open` → `merged`

TinyDesk (or equivalent) handles user-facing notifications on merge. No need to email the submitter.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)