# Design: Convert the dev account into an endpoint-triggered sandbox - **Date:** 2026-05-29 (revised 2026-06-01) - **Author:** Iván Gómez Yaury - **Status:** Draft (pending review) - **Sub-project:** 2 of 2. Sub-project 1 (PRD promotion + cutover) is **complete**: production runs in the PRD account, and the dev account is already parked inert (pollers off). This spec turns that parked dev instance into a useful sandbox. ## Context The production workload now runs in the PRD account (`887841176879`) with the production identities. The dev account (`923929101992`) is parked: it runs but all pollers are off, so it does nothing. This spec makes the dev account a **branch-testing sandbox** — a place to deploy an unmerged branch and exercise the pipelines on real repos, **without interfering with prod**. **Primary use case:** the sandbox exists for developers who cannot run the full pipeline locally (e.g. no local CodeArtifact setup). The point is to **test that the pipelines run**. The hard requirement is **no interference with prod**. The sandbox is driven **only by the manual trigger endpoint** — no autonomous polling — so it can never compete with prod for work. ## Goal Make the dev account an isolated sandbox that runs the pipelines on the real repos via a **manual trigger endpoint**, with all production-polluting side effects suppressed by `SANDBOX_MODE`, its own GitHub App, dedicated Slack channels, and a GitHub Action to deploy any branch. ## Non-Goals - **No autonomous polling.** Dev is endpoint-triggered only; all pollers stay off (Jira, PR-comment, PR-mention, Slack-mention, daily report). No dev Jira account, no assign-and-watch. This is deliberate — it removes any collision risk with prod by construction. - **No separate repos / `repos.dev.json`.** Dev uses the real `repos.json` (replicates prod). Isolation is by identity + `SANDBOX_MODE`, not by repo. - **No provider change here.** Dev runs whatever provider prod runs (Cursor today; Claude after that migration lands). Out of scope. ## Trigger model — endpoint only The dev sandbox is driven exclusively by `POST /hu-agent/api/pipeline/trigger {issueKey}` (and the `/api/pollers/*/tick` endpoints), which are mounted unconditionally — the pipelines are built regardless of poller toggles. A developer fires a specific ticket and watches it run end-to-end. Because nothing polls, the dev instance never picks up work on its own and never competes with prod. When triggered, the fix pipeline **reads** the ticket via the Jira client (to get its description/attachments) — a read only. `SANDBOX_MODE` blocks every Jira **write**, so the real card is never touched. Dev keeps its existing Jira read credentials; no dev Jira account is needed. ## Isolation by identity (per-env config) Both instances share the real `repos.json`; they stay separate by **identity**, overridable per-env via the Terraform `env_vars` merge (`all_env_vars = merge(local.env_vars, var.env_vars)` in `infrastructure/app/main.tf`). ### GitHub — separate dev App The dev instance uses its **own GitHub App, `hu-agent-dev[bot]`**, installed on the real repos (Contents + Pull requests: write). GitHub is required even for endpoint-triggered runs — the fix pipeline pushes branches and opens PRs through it — so the dev App is a **prerequisite**. **Why a separate App (not the shared one with a draft filter):** prod's PR-comment and PR-mention pollers select PRs by `author = `, and there is **no draft filtering in the code** (verified). A "prod skips draft PRs" filter was considered and rejected: a manual draft↔ready toggle would re-mix the two bots. A distinct author (`hu-agent-dev[bot]` vs `hu-agent[bot]`) is the robust boundary — prod's pollers never see dev's PRs regardless of draft state. Configured per-env: dev's `infrastructure/env/dev/main.tf` overrides `GITHUB_APP_ID` / `GITHUB_APP_INSTALLATION_ID` / `GITHUB_BOT_USER` to the dev App's values, and dev's SSM `github-token` holds the dev App's private key. Cross the `IGNORED_COMMENT_LOGINS` (dev ignores `hu-agent[bot]`, prod ignores `hu-agent-dev[bot]`) as a belt-and-suspenders guard. > GitHub is used for more than opening PRs: branch push / git auth, squad labels, and the > PR-comment / PR-mention pipelines (which on dev only run if triggered via the tick > endpoint). The separate App cleanly scopes all of it to dev. ### Slack — same app, new channel(s) No new Slack app. The existing Slack app posts via `SLACK_BOT_TOKEN` to whatever channel IDs are configured. Point dev's channel env vars (`SLACK_CHANNEL_ID`, `SLACK_PR_COMMENT_CHANNEL_ID`, `SLACK_DAILY_REPORT_CHANNEL_ID`, `SLACK_ALERT_CHANNEL_ID`) at the **`hu-agent-test` channel** (`C0B19DPH18R`) — which is already the `SLACK_DEV_CHANNEL_ID` default in `src/core/constants.ts` (the channel dev-mode posts to), so the bot is already a member. This keeps the "Ticket iniciado" monitoring signal separated from prod, without a second Slack identity to maintain. ## `SANDBOX_MODE` (new env flag) A single umbrella boolean `SANDBOX_MODE` (default `false`; `true` only in dev). It gates the side effects that would touch real production state, so an endpoint-triggered run on a real card produces a (draft) PR without dirtying the board: | # | Side effect | Code site | SANDBOX_MODE behavior | |---|---|---|---| | 1 | Jira transition (`transitionIssue` "In Progress"/"To Do") | `fix-pipeline.ts:93`, `:214` | **skip** | | 2 | Jira comment (`addComment`) | `fix-pipeline.ts:202` | **skip** | | 3 | Jira subtask (`createSubtask`) | jira client / fix-pipeline | **skip** | | 4 | PR creation (`createPr`) | `fix-pipeline.ts:597` | **draft PR** | | 5 | Squad labels (`addLabels`) | `fix-pipeline.ts:614` | **skip** | PRD leaves it `false` → behavior identical to today. (The daily report is simply off on dev via the existing `DAILY_REPORT_ENABLED=false`; no `SANDBOX_MODE` logic needed for it.) ### App changes required - **`config.ts`** — add `SANDBOX_MODE` (Zod boolean, default `false`) → `AppConfig.sandboxMode`. - **`index.ts`** — thread `config.sandboxMode` into `FixPipeline` deps. - **`fix-pipeline.ts`** — wrap the gated calls: `if (!sandboxMode) await transitionIssue(...)` for items 1–3 and 5; pass `draft: sandboxMode` to `createPr` for item 4. Log at info when a write is skipped due to sandbox mode, so the dev logs explain the difference. - **`github/client.ts`** — `createPr` gains a `draft?: boolean` param forwarded to `octokit.pulls.create({ draft })` (`client.ts:254`). Default `false` preserves prod. Small, localized guards — not an architecture change. The gating lives in the pipeline; the clients stay dumb. ## Dev secrets & env overrides The dev account's SSM `/hu-agent/*` currently holds the prod credentials. For the sandbox: - `github-token` → the **dev GitHub App** private key (prerequisite). - `slack-bot-token` → unchanged (same Slack app); the channel env vars point at dev channels. - `jira-email` / `jira-api-token` → unchanged (used for ticket **reads** when triggered; `SANDBOX_MODE` blocks all writes). `infrastructure/env/dev/main.tf` `env_vars` override (on top of the shared module defaults): `SANDBOX_MODE="true"`, the dev `GITHUB_APP_ID` / `GITHUB_APP_INSTALLATION_ID` / `GITHUB_BOT_USER="hu-agent-dev[bot]"`, the dev Slack channel IDs, a crossed `IGNORED_COMMENT_LOGINS`, and all poller toggles left off (`JIRA_POLL_ENABLED=false`, `ATTEND_FEEDBACK_IN_PR_ENABLED=false`, `PR_MENTION_ENABLED=false`, `SLACK_MENTIONS_ENABLED=false`, `JIRA_TICKET_REVIEW_ENABLED=false`, `DAILY_REPORT_ENABLED=false`) as they already are post-cutover. No change to the shared `app` module. ## Branch-deploy workflow Repurpose `.github/workflows/dev.yml`: drop the `push: [release]` trigger (that is PRD's job now) and make it `workflow_dispatch`-only with a `ref` input, deploying any branch to the dev account so a developer can test unmerged branches: ```yaml on: workflow_dispatch: inputs: ref: description: "Branch or SHA to deploy (blank = current branch)" required: false default: "" jobs: deploy: uses: ./.github/workflows/deployment.yml with: environment: '{"env":"dev", "acc":"923929101992"}' ref: ${{ github.event.inputs.ref || github.ref }} ``` Note: each redeploy wipes the ephemeral `workdir`, so the first triggered ticket after a deploy re-clones all managed repos (cached thereafter). Optional cost control: keep the dev service at `desired_count = 0` and bump to 1 on deploy — decide at implementation time. ## Documentation (explicit deliverable) - **`README.md`** — "Environments" section: PRD (real production, pollers on) vs the dev sandbox (endpoint-triggered, `SANDBOX_MODE` on, separate GitHub App, dev Slack channels); what `SANDBOX_MODE` gates; how to deploy a branch; how to fire a ticket via the endpoint. - **`AGENTS.md`** (+ `CLAUDE.md` symlink) — short note: `SANDBOX_MODE` exists and what it gates; dev is endpoint-triggered with a separate GitHub App and dev Slack channels. - **`.env.example`** — document `SANDBOX_MODE` and the per-env identity/channel override vars. ## Risks - **`SANDBOX_MODE` gaps** — any future pipeline write path must also be gated. Note as a maintenance rule (reinforced by sub-project 3's Documentation Rules). - **Real (draft) PRs on real repos** — dev opens draft PRs authored by `hu-agent-dev[bot]`; they are real. Consider auto-closing stale sandbox PRs; the separate App keeps prod's pollers from touching them. - **Jira auto-transitions via the GitHub integration (outside `SANDBOX_MODE`)** — `SANDBOX_MODE` only gates the bot's *direct* Jira API writes (`transitionIssue`/`addComment`/`createSubtask`). It does NOT stop **Jira's own "Automation for Jira" / GitHub-integration rules** from moving the card when the dev bot creates a real branch/PR containing the card key (e.g. branch `fix/SQCY-2526-…` → "In Progress"; PR on that branch → "In Review"). So a sandbox run on a **real** ticket still advances that ticket's status via Jira reacting to GitHub events — not via our code. Mitigations (operational, not code): prefer **throwaway/test tickets** for sandbox runs, or **manually reset** the card afterward. Fully avoiding it would require the dev branch/PR to omit a Jira-recognizable key (which breaks the GitHub↔Jira link and is not recommended). Documented as a known limitation. ## Open questions / dependencies - Provisioning the **dev GitHub App** (`hu-agent-dev[bot]`): App id, installation id, private key into dev SSM — prerequisite for any dev run. - Slack: dev posts to `hu-agent-test` (`C0B19DPH18R`), already the dev-mode channel — no new channel needed. - Sub-project 1 cutover is complete (satisfied).