Skip to content

Epic: Integrated dev platform — isolated per-job runners that clone, develop, test, and ship PRs #470

Description

@Weegy

Epic: Integrated dev platform — isolated per-job runners that clone, develop, test, and ship PRs

Motivation

omadia agents can talk about code, but they cannot touch it. There is no path today from "fix issue #123 in repo X" to a reviewed pull request. This epic adds that path: a dev platform that — comparable to a GitHub Actions job or a GitLab Runner — provisions an isolated workspace per job, checks out a git repository, and performs real development work inside it: static analysis, running the test suite, writing code, resolving issues, filing new issues. Results flow back through git: a pushed branch and a PR that a human reviews and merges.

The scope is deliberately "everything a CI job can do to a repo, plus authoring": read, build, test, modify, commit, push, open PRs, file and comment on issues. Merging stays with humans — structurally, not by convention (see security model).

What exists today (seams to build on)

Codebase research summary — the platform already has most of the spine, and none of the execution layer:

Seam Where Relevance
Conductor middleware/src/conductor/ Durable multi-step runs, claim-based resume, human-approval awaits resolved by role-holders. Workflow spine + approval gates.
Plugin Builder pipeline middleware/src/plugins/builder/ Closest analog: LLM codegen → tsc in staging dir (buildSandbox.ts) → auto-fix loop → runtime smoke → install. Host-local, in-memory BuildQueue.
CLI Shape 3 agent harness-orchestrator/src/cliChatAgent.ts + loopbackMcpServer.ts Drives the official claude CLI which owns its own tool loop; omadia currently withholds Bash/FS tools. The obvious coding brain for a runner.
MCP Control Center (#459) middleware/src/services/mcp*, /admin/mcp Grant scoping, risk scanning, fail-closed dispatch, audit at a single choke point — the audit/grant pattern to copy.
GitHub App auth middleware/src/plugins/builder/githubAppAuth.ts RS256 JWT → short-lived installation tokens (today issues:write only). Extend scopes per job instead of adding octokit.
Vault middleware/src/secrets/ Namespaced secret store; MCP stdio servers already get env injected from Vault at spawn.
Plugin API pattern @omadia/plugin-api New capability = manifest permission + ctx accessor (the #459 ctx.mcp rollout is the template).

What does not exist and must be built: any container/sandbox layer (no Docker API usage, no Fly Machines calls anywhere), git clone/checkout, branch/push/PR code, and a durable job queue (ctx.jobs is non-persistent, BuildQueue is in-memory).

Architecture sketch

Execution model: self-driving runner, outbound-only

The runner is self-driving. The middleware provisions it and then never reaches into it — no exec, no shared filesystem. The runner phones home over HTTP with a one-time job token: fetch job spec, stream JSONL events + heartbeats, report the result. This one decision keeps every backend symmetric (a Fly Machine can't accept inbound connections from the middleware anyway) and it survives containerization, which host-touches-workspace designs never do.

interface RunnerBackend {
  provision(spec: DevJobSpec): Promise<RunnerHandle>;
  terminate(handle: RunnerHandle): Promise<void>;
  reap(): Promise<RunnerHandle[]>; // orphan cleanup on boot + interval
}

Backends, in delivery order:

  1. LocalProcessBackend — spawn in a temp workspace on the middleware host, buildSandbox.ts style. Explicitly labeled trusted repos, operator-triggered only; webhook triggers refuse this backend. Exists to de-risk the job spine and agent loop independently of isolation. Must still speak the full phone-home protocol.
  2. DockerBackend via a runner sidecar daemon backed by a dedicated DinD engine — a small compose service with a narrow authenticated API (startJob, streamLogs, kill). The middleware never mounts the host Docker socket: socket-in-middleware makes one middleware RCE a host takeover, which is the opposite of the product's compliance positioning. Instead of the host daemon, the runner daemon points DOCKER_HOST at a dedicated docker:dind engine service in the compose stack (own image-cache volume, internal-only network) — the same pattern GitLab Runner uses. Job containers are nested inside that disposable engine; the host daemon never sees them, the privileged flag is confined to the one dind service, and pre-pulled dev images live in the dind volume (the daemon warms the cache). The daemon clamps the hardening baseline itself regardless of what the spec requests (non-root, all caps dropped, no-new-privileges, read-only rootfs + single writable workspace volume, cpu/mem/pids/disk limits, no host mounts, fresh container per job). For repos whose test suites need Docker themselves (testcontainers, compose-based integration tests): opt-in per-repo rootless DinD sidecar per job — never shared across jobs, never the host daemon.
  3. FlyMachinesBackend — ephemeral Machine per job from the published runner image, destroyed on completion. The only sane isolation on the single-VM Fly topology.

One published omadia/dev-runner image: node + git + ripgrep + pinned Claude Code CLI + the runner shim as entrypoint.

Inside the container: shim + headless coding agent

A thin TypeScript runner shim (omadia-owned) wraps the coding agent:

  1. Fetches the job spec with the one-time token.
  2. Clones the repo shallow with a per-job GitHub App installation token via a git credential helper — the token lands in neither env nor .git/config, both of which repo code reads trivially.
  3. Runs the agent headless (claude -p --output-format stream-json, workspace as cwd) with the job brief as prompt; translates its stream into omadia job events.
  4. On success: branch omadia/job-<id>-<slug>, commit as omadia-dev[bot], push, report branch + diffstat + summary.

The agent kind is a per-job column, so an omadia-native orchestrator loop (or a different CLI) can slot in later without schema or protocol changes. LLM access supports both existing modes: operator provider key (routed through a middleware LLM proxy, see security) and subscription CLI credentials (#309 pattern).

PR creation happens host-side after the runner reports the pushed branch. Forge write credentials never enter the container, and the clone token stays contents-scoped.

Forge layer

In-container: plain git CLI. Host-side: a ForgeClient interface (createPR, getIssue, createIssue, commentIssue) with GitHubForgeClient first, extending the existing hand-rolled githubAppAuth.ts fetch style (App gains contents:write + pull_requests:write). GitLab/Gitea later are one class each, keyed by dev_repos.forge_kind.

Repo onboarding: credentials & tracker binding

Adding a repo is a guided flow: pick the forge, pick a credential mode, optionally bind an issue tracker, run the branch-protection check.

Credential modes (per repo, dev_repos.credential_kind + ref):

  1. GitHub App (recommended). Per-repo installation, short-lived scoped tokens, commits as omadia-dev[bot], and "cannot merge" holds cryptographically. Self-hosted operators don't hand-register anything: the onboarding flow uses the GitHub App Manifest flow — omadia posts an app manifest, GitHub creates the App in the operator's org with one click and redirects back with the credentials, which land in Vault.
  2. Device flow (quick start). Reuses the existing device-flow code (middleware/src/issues/githubOAuthProvider.ts, token in Vault per operator). Zero setup, works immediately. Trade-offs stated plainly in the UI: the token is a user token — commits appear as that user, classic scopes are repo-wide rather than single-repo, and the token can merge, so the no-merge guarantee degrades from token-scope to policy-engine-only. Allowed for trusted repos; webhook triggers stay disabled on device-flow repos.
  3. Fine-grained PAT / deploy key (fallback + generic). Manual paste, per-repo scoping, expiry. This is also the lowest common denominator for GitLab/Gitea, where modes 1–2 don't exist in the same shape.

Tracker binding (per repo, optional): a TrackerClient interface separate from ForgeClientgetTicket, listOpenTickets, commentTicket, transitionTicket, createTicket. GitHub Issues ships built-in (forge and tracker happen to coincide there). Jira and friends arrive as plugins contributing a tracker capability service via the existing capability-service registry — the same mechanism other integrations use, so an enterprise adds Jira without core changes. With a tracker bound, a job can start from a ticket ("run job on PROJ-123"), post its clarification questions as ticket comments, link the PR back, and transition the ticket. Ticket text is hostile input (prompt-injection vector) and flows through the same policy engine as issue text.

The default job pipeline

A dev job with kind fix_issue/implement runs a fixed phase sequence, persisted in dev_jobs.phase:

analyze → plan → clarify → await_human → implement → review → pr
  • analyze — runner clones, reads the ticket + codebase, produces an analysis artifact.
  • plan — implementation plan (files, approach, test strategy) stored in dev_job_artifacts.
  • clarify — the agent lists its open questions. No questions → phase auto-skips.
  • await_human — the gate. Plan + questions surface wherever the operator lives (admin UI, chat, ticket comment); a role-holder answers/approves via a Conductor await. The runner terminates while parked — no container idles on approval latency. Answers are appended to the job brief.
  • implement — a fresh runner session re-clones and executes the approved plan.
  • review — a second agent pass (separate prompt, optionally separate model) reviews the final diff against the plan, plus the middleware-side diff policy engine. Findings either loop back into implement (bounded retries) or annotate the PR description.
  • pr — host-side PR creation, linked back to the ticket.

Single-shot jobs (analyze, small operator-triggered fixes) may run a collapsed pipeline (no gate) — that stays an explicit per-job choice, default is gated.

Job model & durability

Own store; Conductor optional on top. A dev job is a long-lived external process with its own lifecycle — forcing every ad-hoc chat job through Conductor adds ceremony. A runDevJobStep Conductor effect wraps a job as one opaque step where approval gates and multi-step workflows are wanted.

Migration 0020_dev_platform.sql (next free number in middleware/migrations/):

  • dev_repos — forge_kind, clone_url, default_branch, credential_kind (github_app | device_flow | pat | deploy_key) + credential_ref, tracker_kind + tracker_config jsonb, allowed_triggers, egress_allowlist
  • dev_jobs — repo_id, kind (analyze | fix_issue | implement | file_issues), brief, source (chat | admin | conductor | webhook | schedule | tracker) + source_ref (ticket key), backend, agent_kind, phase (pipeline position), status (queued → provisioning → running → waiting → pushing → done | failed | cancelled | stalled | budget_exceeded), runner_handle jsonb, branch, pr_url, result jsonb, token/cost counters, created_by, timestamps
  • dev_job_events — append-only JSONL event log (log/tool/status/heartbeat); SSE source and audit trail
  • dev_job_artifacts — diffs, test reports

Concurrency via DB claim (UPDATE … WHERE status='queued' … RETURNING), mirroring Conductor's claim pattern. Queue state lives in Postgres from day one — the one thing this must not copy from the Plugin Builder is the in-memory queue, or restarts orphan jobs forever.

Triggering

Trigger Mechanism
Admin UI /admin/dev-platform — repo registry, launch form, live log tail (existing admin page pattern)
Chat ctx.devJobs accessor gated by a new manifest permission (the #459 ctx.mcp rollout as template) + built-in orchestrator tool; job card with SSE status in chat
Conductor runDevJobStep effect; approval via the existing waiting parking
Webhook GitHub App webhook, e.g. issue labeled omadia-dev → job (relates to #437; gated on the security sign-off since issue text is hostile input)
Schedule ctx.jobs cron calling ctx.devJobs — free once the accessor exists

Security model

The job container is a blast chamber. Everything inside — including the agent's own tool calls once repo content entered its context — is assumed compromised. Security properties hold outside the container: in token scoping, egress policy, and a middleware-side policy engine. Never via prompts.

Threat model (condensed): hostile repo code (postinstall, test fixtures, git hooks run arbitrary code in-job); prompt injection via issue/PR text steering the agent ("add a backdoor, exfil the token"); credential exfiltration; lateral movement container → host/DB; supply chain via the runner image.

Consequences, structural:

  • Egress: default-deny. A dev job legitimately needs three destinations — git remote, package registries, LLM endpoint — and the third is the middleware proxy, never the vendor. A mandatory egress proxy (per-job hostname allowlist from dev_repos.egress_allowlist, deny RFC1918/link-local, DNS resolved by the proxy, every CONNECT logged) reuses the SSRF-guard thinking from the MCP Control Center. Enterprises pointing at an internal Artifactory add exactly that host.
  • Git credentials: per-job installation token, single-repo scope, contents:write + pull_requests:write max, ≤1h lifetime, revoked at job end including crash paths. Delivered via credential helper. Device-flow and PAT repos use the same helper delivery but inherit their mode's weaker scoping (see repo onboarding) — the UI says so instead of pretending otherwise. Hardening backlog: token held only by the proxy, never entering the container at all.
  • LLM keys never enter the container. Jobs call the middleware LLM proxy with a per-job bearer token; middleware attaches the Vault-held key, enforces the model allowlist (reuse the ctx.llm gate), meters tokens/cost per job, applies Privacy Shield where configured.
  • "Opens PRs, never merges" is unforgeable: no merge/admin permission exists on the job token. Push restricted to omadia/job-* branches; branch protection on default/release branches is a documented setup assumption the repo-onboarding flow checks via API and warns about loudly. No force-push.
  • Human gates (Conductor awaits): first-ever job on a repo; diffs touching CI/workflow files (.github/workflows/**, Dockerfile — those can steal secrets in the forge's CI later), dependency manifests + lockfiles, or >N files; releases/tags; cross-repo issue filing. Everything else runs autonomous: analyze, test, commit to the job branch, open the PR, comment on its own PR/issue.
  • Diff policy engine runs middleware-side on the final diff before push is permitted — code, so it cannot be prompt-injected.
  • Audit (MCP audit pattern, job = choke point): job spec + trigger + actor, every command (argv, exit code, duration), full egress log, LLM call hashes (full transcript operator-toggleable, default on for enterprise), final diff always, token mint/revoke, every approval with resolver identity. Log scrubber redacts job tokens before persistence. Retention configurable, JSONL export for SIEM.
  • Budgets & reaping: hard wall-clock limit (default 30 min) and LLM cost budget enforced by the proxy → SIGTERM/SIGKILL → budget_exceeded. The runner daemon labels containers with jobId; a reconciler kills anything whose job is terminal or whose lease expired. Janitor deletes job branches with no PR after 24h.

Non-negotiable in v1: no Docker socket in the middleware, full container hardening baseline, default-deny egress, per-job scoped tokens with revocation, LLM proxy, token-scope-based no-merge, protected-path human gates, per-job audit with secret scrubbing, budgets, zombie reaper.
Deferrable: remote runner-host registration (GitLab-Runner style), gVisor/Kata/Firecracker (hardened runc is acceptable v1, documented), git wire-protocol inspection, image signing/SBOM automation (pin digests manually), transcript search UI.

Wave plan

Full implementation specs are attached as comments — see the spec index.

The specs were adversarially reviewed (findings) and have been rewritten to v2 (status). All five blockers are closed in the specs themselves — migration chain 0021→0024, CHECK constraints dropped, provision column fixes the seq collision, the Conductor await is redesigned, and the LLM proxy is a W1 deliverable. Two structural corrections shape the plan below: isolation is part of W0, and the runner never pushes — the middleware applies the reviewed diff server-side through the forge git-data API. The execution flow describes how the waves get built (spec → unit manifest → worktree-isolated implementation → cross-family adversarial review → human gate).

All open questions are now answered (question 4 decision). W0 is ready for implementation.

  • W0 — Job spine + isolation + repo onboarding. Migration 0021, DevJobStore + claim loop, RunnerBackend interface, runner shim, headless CLI agent, clone + diff upload, repo-add flow with device-flow quick start (reusing the existing device-flow code) and PAT fallback, GitHub Issues tracker read (ticket → job brief), server-side commit + PR creation from the reviewed diff (the runner never holds a write token), minimal /admin/dev-platform page with live log tail. Shippable: operator adds a repo, picks issue fix(manifests): use canonical key/help in setup fields so secrets render #123 → gets a PR. Isolation is not deferrable: either the container path ships here, or the LocalProcessBackend runs under a dedicated unprivileged UID with no Vault/DB/CLI-credential access and executes neither dependency install nor the repo's test suite, behind an explicit DEV_PLATFORM_UNSAFE_LOCAL=true. An agent with --dangerously-skip-permissions must never run untrusted repo code on the middleware host.
  • W1 — Container platform + LLM proxy. omadia/dev-runner image published, runner sidecar daemon + dedicated DinD engine service in the compose stack, DockerBackend, hardening baseline enforced daemon-side (env and egressAllowlist derived server-side from the dev_repos row, never from the request body; daemon on a middleware-only network, not the shared bridge), image-cache warming, egress proxy with per-repo allowlist and a resolver that answers only allowlisted names, cancellation + reaper, the middleware LLM proxy (per-job bearer → Vault key attach → streaming passthrough → usage extraction), and the golden-fixture end-to-end CI harness. Gated on the option-D spike (does the CLI honor ANTHROPIC_BASE_URL?).
  • W2 — GitHub App onboarding + pipeline v1. GitHub App manifest-flow repo onboarding (recommended credential mode), per-job scoped tokens with revocation via a single finalizeDevJob choke point, a bootstrap phase (project-type detection, dependency install with its own timeout and egress budget, package cache across provisions) without which "run tests" is not executable, the phased pipeline (analyze → bootstrap → plan → clarify → await_human → implement → review → pr) with the gate surfaced in the admin UI, plan-hash + base-commit pinning at approval, per-repo launch authorization, whole-job retry from the persisted phase, tracker comment-back.
  • W3 — Chat/plugin surface + Conductor + policy engine. ctx.devJobs accessor + manifest permission, built-in orchestrator tool, SSE job cards in chat, gate resolution from chat, runDevJobStep Conductor effect (one await for the step's lifetime; gate answers flow through the job's own gate API, not resolveAwait), diff policy engine gating the server-side apply — extended to scan the PR body and tracker comments for secret and high-entropy patterns, plus a non-overridable deny rule for credential-shaped content — and the tracker capability-service interface so adapters (Jira first) ship as plugins.
  • W4 — Fly Machines + webhooks + accounting. FlyMachinesBackend, label-triggered jobs from GitHub webhooks (Webhook support (inbound and outbound) #437) with the sender checked against a per-repo allowlist, per-repo and per-sender rate limits, and a human gate on the first job from any new trigger source; tracker-triggered jobs, token/cost surfaced per job, LLM proxy budgets, GitLabForgeClient stub.
  • W5 — Hardening backlog. Rootless per-job DinD sidecar for docker-needing test suites, remote runner hosts, image signing/SBOM, transcript tooling, data lifecycle (event retention + per-job cap, artifact size ceiling with blob offload, purge policy for terminal jobs). The proxy-held git token drops out of scope — with server-side apply there is no git token in the container to hold.

Non-goals (v1)

No general CI replacement (matrices, caches, artifact pipelines — the repo's own CI keeps doing that). No DinD or image builds inside jobs. No multi-agent swarms per job. No K8s backend. No in-omadia diff-review UI — the forge PR page is the review surface. No auto-merge, in any wave.

Riskiest bets

Standardizing on the Claude Code CLI headless as the in-container agent. Its stream-json protocol and subscription-credential handling inside a container are the least-controlled dependency here; a breaking CLI change would stall the platform. Mitigations are structural: the shim owns the process boundary and event protocol, the CLI version is pinned in the runner image, and agent_kind is per-job so an omadia-native loop can slot in without schema changes.

Open questions

Resolved by the wave specs (rationale in each spec; summary in the spec index):

  1. Runner daemon transport/auth: mTLS vs shared secret?shared bearer secret in W1; mTLS with remote runner hosts in W5.
  2. Default N for the ">N files touched" human gate?50, per-repo configurable via policy_overrides.
  3. Egress proxy packaging?one compose service between the dind engine and the world; per-job separation via proxy credentials + allowlists.
  4. Where does the runner image live?GHCR under byte5ai, versioned with the middleware release, launched by digest.
  5. DinD engine storage / eviction? → volume-bounded + prune-on-warm; per-job storage-opt quotas deferred (needs xfs+pquota in dind).
  6. clarify phase UX: always mirrored to the ticket?whenever the job is bound to a ticket, regardless of trigger. Answers never parsed from issue replies (prompt-injection surface).
  7. Review phase: same model or cross-model?same CLI, fresh session, adversarial reviewer prompt; cross-model later via the per-job agent_kind column.

4. Still openDECIDED: subscription-CLI credentials inside a job container

Answer (full analysis): API key through the middleware LLM proxy is the default and the only mode for jobs that execute repository code. Subscription mode is permitted only in the no-execution jail (no dependency install, no test run), operator-triggered, container backend, behind DEV_PLATFORM_SUBSCRIPTION_MODE=false-by-default, and its diff always parks at a human gate because it is not execution-verified.

Option D is rejected, not deferred. Between January and April 2026 Anthropic blocked OpenClaw, OpenCode, Roo Code and Goose from subscription auth. The line: extracting the OAuth token and using it from a third-party client is prohibited; spawning the real claude CLI subprocess is not. #309 sits on the permitted side. A middleware that holds the OAuth credential and injects the header moves omadia to the prohibited side. The surviving half of the spike — does claude -p honor ANTHROPIC_BASE_URL under an API key? — is documented as yes; it becomes an integration test in W1's golden-fixture harness rather than a gate.

Enforcement: dev_jobs.auth_mode, an admission check (subscription && executes-repo-code → refuse) in both the create route and the worker, operator-trigger-only, no Fly backend, CLAUDE_CODE_OAUTH_TOKEN injection (never a mounted credentials file), a distinct egress allowlist, and a mandatory gate before apply. byte5 warrants API-key mode only.

The original framing, retained for the record:

The question: may a dev job run the claude CLI on the operator's Claude Pro/Max subscription (#309), which requires the subscription's OAuth credential to live inside the job container — or must every dev job use an API key routed through the middleware LLM proxy?

Why the two LLM modes are not symmetric. With an API key, the key stays in the middleware: the container gets ANTHROPIC_BASE_URL pointed at the middleware proxy plus a per-job bearer, and the middleware attaches the real key, meters tokens, enforces cost budgets, and applies the model allowlist. With a subscription, the CLI authenticates itself from the credentials claude auth login --claudeai wrote into CLAUDE_CONFIG_DIR (middleware/src/platform/cliAuthService.ts). There is no per-job minting, no scoping, no short lifetime — and no proxy, because cliChatAgent.ts deliberately scrubs ANTHROPIC_BASE_URL / ANTHROPIC_CUSTOM_HEADERS from the CLI env (CLI_ENV_SCRUB_KEYS, "could redirect to a metered gateway/proxy"). Running the CLI headless in a container therefore means copying an account-wide, long-lived credential into the blast chamber.

Four consequences:

  1. It would be the only secret in the design with durable value. Every other secret a job sees is per-job, minutes-lived, and revocable (git token, job token, W5 relay nonce). The subscription credential is account-wide and sits in the same container where hostile repo code executes — postinstall scripts, test fixtures, git hooks. Exfiltration hands an attacker the operator's Claude subscription. Note a second exfil path beyond network egress: a steered agent can simply commit the credential file into the job branch.
  2. Cost budgets stop working. W4 enforcement lives at the proxy (402 → budget_exceeded → terminate). Subscription jobs bypass it, leaving only the CLI's self-declared usage (usage_estimated = true) and the wall-clock watchdog. Jobs become time-bounded but not cost-bounded.
  3. The login session is host-global. cliAuthService permits exactly one active session on the persisted volume. All dev jobs would share one subscription identity — and share it with the Subscription-backed agents via official CLIs (Claude / Codex / Gemini) using an MCP tool bridge #309 interactive chat agents, rate limits included. Revocation is coarse: claude auth logout kills the whole CLI integration.
  4. Terms of use. Pro/Max subscriptions target interactive use. A webhook-triggered, unattended runner in server infrastructure, shipped to enterprise operators, is a different thing. This needs an answer before the feature ships; it is not a question the codebase can settle.

Options:

Option Consequence
A Block subscription mode for dev jobs; require an API key (or another provider) through the proxy Clean, but excludes exactly the #309 audience — self-hosters on Claude Max without an API key
B Allow, structurally restricted Enforceable: only source='admin' (operator-triggered) jobs, only on repos flagged trusted, never webhook/tracker/chat-triggered, refused on the Fly backend (weaker egress enforcement). Requires a deny rule in the W3 diff policy engine for credential-shaped content. Budgets degrade to wall-clock only, stated in the UI
C Allow generally with mitigations Not defensible — the available mitigations (tmpfs, egress allowlist, throwaway account) are weak against in-container code

Option D — a spike that may dissolve 1 and 2. Does the CLI accept an ANTHROPIC_BASE_URL override while running in OAuth subscription mode? The scrub in CLI_ENV_SCRUB_KEYS exists for billing precedence (don't silently bill an API key instead of the subscription), not as evidence that the CLI refuses a base-URL override under OAuth. If it works, the middleware can hold the OAuth credential and inject the Authorization header: the container gets only a per-job bearer, the credential never enters it, and metering becomes possible again. Consequence 4 (ToS) survives regardless.

Recommendation: ship option B as the default, run the option-D spike before the W1 image is frozen (a positive result lets B's restrictions relax later), and settle the ToS question separately. Blocks: W1 image contents, W4 Fly egress story, W4 budget enforcement semantics.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions