Skip to content

A remote caller's leased device reaches ports on the caller's machine, and back #1110

Closed
5 of 5 issues completed

Description

@leonardoxr

Request: #1100

Problem

In the reporter's words: "exec runs its command on the worker. So adb reverse tcp:5173 tcp:5173 or adb forward from a remote agent connects the device to port 5173 on the worker, not on the agent's machine. The device can't reach a dev server or a candidate build served on the caller's machine. Every device test of a local build has to move to the worker, which also competes with the timing measurements we run there."

Who it is for

A coding agent or CI job on a machine other than the worker, holding a lease on an Android emulator or an enrolled Android phone through a gateway or a lone worker's HTTP API, that serves its app under test locally (a Vite dev server, a mock API, a local web build) or wants to reach a port on the device (a debugger, an app's own server).

Outcome

  • On the caller's machine, simlock tunnel reverse <device-port>[:<caller-port>] makes the leased device's localhost:<device-port> reach the caller's localhost:<caller-port>, and forward <device-port>[:<caller-port>] makes the caller's localhost:<caller-port> reach the device's <device-port>. One command opens several; the caller port defaults to the device port.
  • It works through a gateway and against a lone worker, over the same HTTP address and agent token the caller already uses. No new inbound port opens anywhere.
  • The tunnels live while the command runs. Stopping it, or losing its connection, closes them and removes what they set up on the device; the lease is untouched. The lease ending stops the command.
  • Only the lease's holder (and an operator) can open a tunnel on it. The caller side only ever touches loopback.
  • It is off unless an operator turns on capabilities.tunnel.enabled on the gateway and the workers.
  • Android emulators and enrolled Android phones are covered; any other lease is refused.
  • After this feature, no adb reverse rule on an Android device, and no adb forward rule the adb server holds for that device, survives into the next lease or a worker restart.

How it works for the user

flowchart TD
  A[Agent on machine B holds an Android lease] --> B[simlock tunnel reverse 5173]
  B --> C{tunnel capability on, lease Android, port free, under 8 tunnels?}
  C -->|no| E[error line, exit non-zero, nothing left open]
  C -->|yes| D[Tunnels open; command stays in the foreground]
  D --> F[Device opens localhost:5173]
  F --> G{caller listening on 5173?}
  G -->|yes| H[bytes flow both ways]
  G -->|no| I[device connection refused; one warning line, at most every 10 s]
  D --> J{Ctrl-C, lost connection, or lease ends}
  J --> K[tunnels closed, device rules removed]
Loading

Before, from machine B:

$ curl -X POST -H "Authorization: Bearer $T" .../v1/leases/w1.3f9c/exec -d '{"tool":"adb","args":["reverse","tcp:5173","tcp:5173"]}'
# the device's localhost:5173 now reaches port 5173 on the worker, where nothing runs

After:

$ export SIMLOCK_URL=https://gateway.example SIMLOCK_TOKEN=sk_agent_…
$ simlock tunnel reverse 5173 forward 9229
reverse device:5173 -> localhost:5173 (lease w1.3f9c)
forward localhost:9229 -> device:9229 (lease w1.3f9c)
reverse 5173: nothing listening on localhost:5173          # on stderr, if the dev server is down
^C
$ simlock tunnel reverse 8081 --lease w1.7a2e              # an iOS simulator lease, exit 1
{"error":{"code":"TUNNEL_UNSUPPORTED","message":"Lease w1.7a2e: the lease is on an iOS simulator; tunnels need an Android emulator or phone"}}
$ simlock tunnel reverse 8081                              # 8081 already reversed on the device, exit 1
{"error":{"code":"TUNNEL_PORT_IN_USE","message":"Lease w1.3f9c: reverse 8081: the device port is already reversed; remove that rule first"}}

Examples

Tunnel argument Today After
reverse 5173 no command device 5173 → caller 5173
reverse 8081:3000 no command device 8081 → caller 3000
forward 9229 no command caller 9229 → device 9229
forward 9229:19229 no command caller 19229 → device 9229
reverse 80 no command refused by the worker: TUNNEL_BAD_PORT
reverse 70000, reverse abc, reverse 5173: no command usage error
nine tunnels on one lease — refused: at most 8 per lease
Lease on Today (adb reverse through exec) After (simlock tunnel)
Android emulator reaches the worker reaches the caller
enrolled Android phone reaches the worker reaches the caller
iOS simulator, iPhone no adb TUNNEL_UNSUPPORTED

What could go wrong

  • A dev server that answers only on the caller's LAN address, not loopback, is not reached. Likely now and then; the docs say the server must listen on localhost.
  • A large build served through the tunnel loads slower than on the worker: every byte crosses the gateway. Expected; the docs say so.
  • An operator turns the capability on and a lease holder reaches a device port they did not expect. The holder already runs adb on the device through exec, so no new power.
  • A rule an agent made by hand through exec now disappears at lease end. Intended.

Words used

  • tunnel: one reverse or forward between a device port and a caller port, open while simlock tunnel runs, e.g. reverse 5173.
  • reverse: the device connects out; its localhost:<device-port> reaches the caller's localhost:<caller-port>.
  • forward: the caller connects in; its localhost:<caller-port> reaches the device's <device-port>.
  • caller: the machine simlock tunnel runs on, usually not the worker.
  • capability: an optional part of Simlock an operator turns on in config, here capabilities.tunnel.enabled.

Non-goals

Completion conditions

  • simlock tunnel reverse <p> against a lone worker makes a page served on the caller's localhost:<p> load on a leased Android emulator (slow lane, emulator).
  • Through a gateway, simlock tunnel reverse <p> forward <q> does the same, and a server on the emulator's port <q> answers on the caller's localhost:<q> (slow lane, emulator).
  • The CLI and a lone worker with fake devices agree on the socket protocol (fast end-to-end test).
  • A tunnel on an iOS lease, a used device port, a port below 1024 or a ninth tunnel fails at once with its error, and nothing stays open.
  • Stopping the command removes its rules from the device; the lease ending stops the command; no reverse rule, and no forward rule the server holds for the device, survives Android reclaim.
  • With capabilities.tunnel.enabled off, the route answers as an unknown route on a worker and TUNNEL_UNAVAILABLE through a gateway whose worker has it off.
  • The CLI reference, configuration reference and remote-agents page describe tunnels.

Open questions

none

Decisions

Shared values

Value or state Rule Written by Read by Source
route and gate GET /v1/leases/{id}/tunnel, a WebSocket owned by module tunnel; on only under capabilities.tunnel.enabled (default false on every role); answers the lease's holder and operators, others 404 as an unknown lease #1112 (worker), #1113 (gateway) #1114, #1115 src/input/definition.ts:12-18, src/core/config.ts:923
frame codec src/contract/tunnel-frames.ts, a contract helper with no wire shape of its own (listed for the protocol-bump check): one binary WebSocket message is [tag u8][conn u32 big-endian][payload], at most 65541 bytes; conn 0 is the control stream; connections the worker opens are odd, the caller's even, each side's ids rising #1112 #1113, #1114, #1115 ADR 0054 §3; src/contract/AGENTS.md (helpers)
control messages (conn 0) 0x01 HELLO caller→worker, JSON of at most 4 KiB {tunnels:[{kind:"reverse"|"forward", devicePort}]}, the socket's first message, once; 0x81 READY worker→caller, the same list; 0x82 REFUSED worker→caller, JSON {code, message}, then close 4003 #1112 #1114, #1115 ADR 0054 §2
connection messages 0x02 OPEN JSON {kind, devicePort}; 0x03 DATA 1 to 65536 bytes; 0x04 CREDIT u32 bytes granted; 0x05 END half-close; 0x06 RESET JSON {reason}; each direction of a connection starts with a 262144-byte window and the sender never has more than that unacknowledged; a reader grants CREDIT as it consumes #1112 #1114, #1115 ADR 0054 §3
bad frames malformed JSON, an unknown tag, a frame before HELLO, a second HELLO, DATA past the granted window, a running window above 2^31, or any frame naming a connection id the other side never opened (wrong parity, or above the highest it opened): REFUSED {code:"BAD_REQUEST"} and close 4003. A frame for a connection already closed is ignored #1112 #1113, #1114, #1115 new
limits 8 tunnels per lease across its sockets (a HELLO that would pass 8, a 9-entry one included, is TUNNEL_LIMIT); 256 open connections per socket, both directions counted together (the 257th OPEN is answered RESET {reason:"too many connections"}); device ports 1024 to 65535, checked by the worker only; fixed, not config #1112 #1114, #1115 ADR 0054 §2-3
error codes (contract) added to the contract's error table, one protocol bump in #1112: TUNNEL_UNSUPPORTED 422 exit 1, TUNNEL_PORT_IN_USE 409 exit 1, TUNNEL_BAD_PORT 422 exit 1, TUNNEL_LIMIT 409 exit 1, TUNNEL_FAILED 502 exit 1, TUNNEL_UNAVAILABLE 503 exit 1, TUNNEL_DISCONNECTED 503 exit 1, TUNNEL_CALLER_PORT_IN_USE 409 exit 1, TUNNEL_LEASE_ENDED 410 exit 14 #1112 #1113, #1114, #1115 src/contract/errors.ts:289-340 (neighbours)
refusal messages name no lease id (through a gateway the worker knows only its own); the CLI prefixes Lease <the caller's id>: . TUNNEL_UNSUPPORTED the lease is on <an iOS simulator|an iPhone>; tunnels need an Android emulator or phone; TUNNEL_PORT_IN_USE reverse <p>: the device port is already reversed; remove that rule first; TUNNEL_BAD_PORT <kind> <p>: device ports are 1024 to 65535; TUNNEL_LIMIT the lease already has <n> tunnels; at most 8; TUNNEL_FAILED <kind> <p>: <adb's message>, or no free tunnel port on the worker when the reserved range is used up; TUNNEL_UNSUPPORTED before #1115 for a forward tunnel: forward tunnels are not available yet; TUNNEL_UNAVAILABLE (gateway, 503 before the upgrade) Tunnels are off on worker <workerId>; turn on capabilities.tunnel.enabled there; BAD_REQUEST <what was wrong> #1112, #1113 (TUNNEL_UNAVAILABLE) #1114, #1115 new; refusal pattern src/input/refusal.ts:10-20
close codes 4001 lease ended (reason: explicit, killed, expired, device-lost, or lease ended when a gateway saw the lease go without the worker's close); 4003 after REFUSED; 1011 the lease's worker stopped answering or is stopping (as the input socket reports a stopping worker); 1001 the daemon that holds the caller's socket is stopping #1112, #1113 #1114, #1115 src/input/AGENTS.md:40-46, src/input/gateway-link.ts:93-94
relay channel through a gateway the socket rides a two-way relay as the input socket does: the worker's lease id is the relay's lease argument; frames wrapped [len u32][tag u8][payload]; the worker sends a heartbeat 0xF0 every 5 s; 0xF1 {code, reason} carries the close; the gateway relays frames up to 65541 bytes and closes a caller that sends a bigger one with REFUSED BAD_REQUEST and 4003 #1113 (both sides) — src/input/framing.ts:7, src/input/worker-link.ts:58-59, src/gateway/relay.ts:46
device-port context the daemon gives the module ctx.deviceTcp, keyed like ctx.inputs.openInput: supports(platform, device); reverse(platform, device, devicePort, signal) → {hostPort, onConnection(listener), close()}, with a loopback listener on a free port of the loopback ports 47000 to 47999, reserved for tunnels (a busy one is skipped), and adb reverse --no-rebind; connect(platform, device, devicePort, signal) → a byte stream with half-close (adb host protocol tcp:<p>); the Android driver implements them for emulators (its configured adb server port) and phones (5037, after the default-server gate); the fake driver implements them for tests #1112 (supports, reverse), #1115 (connect) #1112, #1115 src/modules/types.ts:190-197, src/drivers/android/AGENTS.md:126
byte streams the ports TcpStream gains end() (half-close); the new loopback TcpListener (listen on 127.0.0.1:0 or a named port, accept, close; an in-use port is its own error) hands out the same stream type #1112 #1115 src/ports/tcp-stream.ts:10-14
reverse rule cleanup the module removes its rules on socket close, awaited and bounded at 10 s, before lease end lets reclaim run; a removal that fails keeps its loopback port reserved (connections closed at once) until reclaim. Android reclaim, after the device is proved its own, runs adb -s <serial> reverse --remove-all and removes each adb forward --list line <serial> <local> <remote> for that serial with adb forward --remove <local>; it never runs forward --remove-all. At daemon start, before any lease is served, the Android driver reads adb reverse --list on every emulator it reattaches and proves its own and every enrolled phone present; on an idle device it removes each rule whose host side is tcp: in 47000 to 47999; on a leased device it removes nothing (safety rule 2) and marks those ports taken so no new tunnel listener is given one, and the device's reclaim removes them: the range is reserved for tunnels (the docs tell holders not to point their own rules there; one that does is removed too), and no tunnel survives a restart; rules pointing anywhere else, a holder's own through exec, stay (leases kept across a restart, ADR 0019, keep them) #1112 — src/drivers/android/index.ts:1133-1140, src/drivers/android/index.ts:890-898
caller settings --url or SIMLOCK_URL (an https:// URL, or http:// with host 127.0.0.1, [::1] or localhost); the global --token or SIMLOCK_TOKEN (the credential the command sends as Authorization: Bearer; its help text says so); --lease <id>, else, for an agent token (its role read from GET /v1/identity), its only lease from GET /v1/leases; an operator token must name --lease; the WebSocket client is ws, through a ports adapter in the barrel that loads ws with import() on first use, so no other CLI command and not the MCP server ever load it (Node's built-in client hides a refused upgrade's status and body, and server pings) #1114 #1115 src/cli/index.ts:45, src/cli/index.ts:60
caller ports reverse's caller port 1 to 65535 (connected to); forward's 1024 to 65535 (listened on); no caller port twice in one command, across directions; usage errors #1114, #1115 — new
CLI output stdout, one line per tunnel once READY: reverse device:<d> -> localhost:<c> (lease <id>), forward localhost:<c> -> device:<d> (lease <id>); stderr warning reverse <d>: nothing listening on localhost:<c>, at most once per 10 s per tunnel #1114 (reverse), #1115 (forward) — new
CLI exits and bounds 0 on Ctrl-C or SIGTERM, after sending the close (waited at most 2 s); 1 on a refusal (its code), a lost connection (TUNNEL_DISCONNECTED, Lease <id>: the connection to <url> was lost; no message or ping for 45 s counts as lost; no READY or REFUSED within 90 s of HELLO counts as lost) or a busy caller port (TUNNEL_CALLER_PORT_IN_USE, forward <d>: localhost:<c> is already in use); 2 on a usage error; 14 when the lease ends (TUNNEL_LEASE_ENDED, Lease <id> ended: <reason>) #1114 #1115 src/contract/errors.ts:216-217, src/cli/AGENTS.md:85

Behaviours

Behaviour (invoker, action, what it sees) Seam Cases Task
A holder opens the tunnel socket on a lone worker, sends HELLO with reverse tunnels and gets READY; a device connection arrives as OPEN and bytes flow both ways under credit worker module over a lease socket, fake device-tcp one tunnel; two tunnels; data both ways; END from each side half-closes; a reader that grants no credit pauses only its connection; the 257th connection is reset #1112
A holder's HELLO that cannot be met is refused whole with REFUSED and close 4003, and nothing it named stays set up worker module iOS lease TUNNEL_UNSUPPORTED; reversed port TUNNEL_PORT_IN_USE; port 80 and 70000 TUNNEL_BAD_PORT; a ninth tunnel across two sockets and a 9-entry HELLO TUNNEL_LIMIT; adb error TUNNEL_FAILED after an earlier tunnel opened (it is removed); a forward tunnel before #1115 TUNNEL_UNSUPPORTED #1112
A holder that sends a bad frame is refused BAD_REQUEST and closed 4003, while a frame for an already closed connection is ignored worker module frame before HELLO; second HELLO; unknown tag; DATA past the window; id never opened; CREDIT for a connection just closed → ignored, other connections go on #1112
When the socket closes, the lease ends or the daemon stops, the worker removes the tunnel's rules and listeners and closes its connections worker module client closes; lease ends → 4001 with the reason, rules removed before the hook returns; drain → 1001; a removal that fails keeps the port reserved #1112
A caller reaches the tunnel route on a lone worker only when it may daemon HTTP and upgrade wiring capability off → 404; another holder's lease → 404; holder → upgraded; operator → upgraded #1112
Android reclaim and daemon start leave no stale rule, and never touch another lease's Android driver reclaim, emulator; reclaim, phone; a rule made through exec; another device's forward on the same server stays; reclaim of an emulator not proved its own removes nothing; start removes rules into 47000-47999 on idle owned devices; on a leased device it removes nothing and the ports its rules point to are skipped by the next reverse; a leased device's rule to another port stays; a removal on an already-aborted lease-end hook is skipped (reclaim does it) #1112
The Android driver's reverse sets adb reverse --no-rebind to a loopback listener and removes it on close Android driver device-tcp configured emulator server port; phone port 5037 behind the gate; the listener's port is in 47000-47999, a busy one skipped, all busy → TUNNEL_FAILED; cannot rebind existing socket → TUNNEL_PORT_IN_USE; another adb failure → TUNNEL_FAILED #1112
A holder opens the tunnel socket through a gateway and it is relayed to the lease's worker daemon end-to-end with a real gateway relay and a fake worker device relayed HELLO/READY and a full 65541-byte DATA; REFUSED → 4003; a caller frame over 65541 bytes → 4003; worker capability off → 503 TUNNEL_UNAVAILABLE; worker stops → 1011; lease ends → 4001 with the worker's reason; gateway drains → 1001 #1113
The gateway's tunnel bridge bounds every wait on the worker gateway side with a fake relay and a fake clock no head in 10 s → 503 WORKER_UNREACHABLE; the relay ends for silence → 1011; lease removed and no worker close within 2 s → 4001 lease ended #1113
The worker's relay door keeps an idle tunnel's relay alive worker relay door with a fake clock an idle socket sends a heartbeat every 5 s; a close sends 0xF1 {code, reason} #1113
A user runs simlock tunnel reverse … on another machine and sees one line per tunnel, then the device reaches its loopback port CLI command, fake ports 5173; 8081:3000; two reverses; flags vs env; --lease vs an agent token's only lease vs none vs several; operator token without --lease → usage error; nothing listening → one warning per 10 s, tunnel stays; a 1 MiB response arrives whole (credit granted) #1114
A user's simlock tunnel that cannot run exits with its code and one JSON error line CLI command, fake ports usage errors (70000, abc, 5173:, no tunnel, same caller port twice); http:// to a non-loopback host; 401 on the upgrade; refusal → exit 1 with its code (reverse 80 → TUNNEL_BAD_PORT); lease ends → exit 14; no message for 45 s → TUNNEL_DISCONNECTED; no READY in 90 s → TUNNEL_DISCONNECTED; an idle tunnel with server pings stays open past 60 s; a refused upgrade shows its code (401, 404 UNKNOWN_LEASE, 503 TUNNEL_UNAVAILABLE); lease ends → TUNNEL_LEASE_ENDED, exit 14; Ctrl-C and SIGTERM → close sent, exit 0 #1114
The CLI against a lone worker with fake devices opens a reverse tunnel, carries bytes, and its rules are gone after Ctrl-C daemon end-to-end with real HTTP, the CLI command and the fake driver's device-tcp reverse ok; refusal reaches the CLI as its code; Ctrl-C removes the rule #1114
On a real emulator against a lone worker, a page served on the caller loads on the device through reverse slow lane e2e/slow-android-tunnel.test.ts reverse #1114
A user runs simlock tunnel forward … and the caller's loopback port reaches the device port daemon end-to-end with real HTTP, the CLI command and the fake driver's device-tcp 9229; 9229:19229; the line printed per tunnel; caller port already in use → exit 1 before HELLO; caller port below 1024 → usage error; reverse 5173 forward 5173 → usage error (same caller port across directions); device port closed → the caller's connection closed; caller END half-closes the device stream; connections accepted before READY wait, and close on REFUSED; listeners closed after Ctrl-C, REFUSED, 4001 and a lost connection; forward-only HELLO on an iOS lease → TUNNEL_UNSUPPORTED #1115
The worker validates caller-opened connections worker module OPEN naming a port that is not one of the socket's forward tunnels, an odd or reused id, or kind reverse → BAD_REQUEST, 4003; caller and device connections share the 256 #1115
The worker opens a device port through adb's host protocol for each forwarded connection Android driver device-tcp configured emulator server port; phone behind the gate (gate closed → RESET with its message); tcp:<p> answers FAIL → RESET; no answer in 5 s → RESET {reason:"timeout"} #1115
On a real emulator through a gateway, reverse and forward both work, both ways slow lane e2e/slow-android-tunnel.test.ts reverse through a gateway; forward through a gateway with a device server that answers #1115

Tasks

Sub-issues: #1112, #1113, #1114, #1115 (not linked as GitHub sub-issues yet: the author has no write access; a maintainer links them).

flowchart LR
  R[#1107: device.rotate] --> T1
  T1[#1112: contract codes and codec, tunnel module on a worker, reverse, cleanup] --> T2[#1113: gateway relay, both sides]
  T1 --> T3[#1114: simlock tunnel, reverse]
  B[#1108: simlock rotate] --> T3
  T3 --> T4[#1115: forward]
  T1 --> T4
  T2 --> T4
Loading

Completion conditions: the first is #1114's Done when (slow lane, lone worker); the second #1115's (slow lane, gateway); the third #1114 and #1115; the fourth #1112, #1114 and #1115; the fifth #1112 and #1114; the sixth #1112 and #1113; the seventh each task's docs (configuration and remote-agents for a lone worker #1112; remote-agents through a gateway #1113; CLI reference #1114, extended by #1115).

Open work in the same area:

Written by an agent.

Activity

  1. leonardoxr commented on Oct 31, 2026

    @leonardoxr
    ContributorAuthor

    Spec approved: the feature, ADR 0054 (PR #1111, now Accepted — not yet implemented) and tasks #1112, #1113, #1114, #1115, each checked ready.

    The author has no write access to this repository, so a maintainer still needs to:

    Delivery order: #1111 (ADR) at the bottom of the stack; #1112 after #1107 (both bump the protocol and change WorkerContext); then #1113 and #1114 (after #1108) side by side; #1115 last. #1114 and #1115 add e2e/slow-android-tunnel.test.ts, which needs a slow lane with the Android SDK.

    Written by an agent.

  2. V3RON commented on Nov 1, 2026

    @V3RON
    Contributor

    Delivery run

    Merged: #1121 (#1112), #1125 (#1113)
    Parked: #1114 — review cap reached; R4 fixed after the final fix-check (50c1f69, two test assertions), unreviewed
    Waiting: #1115 — on #1114; #1111 (ADR 0054) — flips to Accepted and merges after #1115
    Decided: #1112 — phone cleanup → emulators only, a phone loses only its tunnel's own rules; #1112 — loopback listener → no check, exposure documented; #1112 — stale phone rule → its port stays reserved; #1112 — aborted lease end on a phone → still removes the tunnel's rules

    Written by an agent.

  3. V3RON commented on Nov 1, 2026

    @V3RON
    Contributor

    Delivery run

    Merged: #1121 (#1112), #1125 (#1113), #1126 (#1114)
    Parked: #1115 — needs-hardware: reviewed and fixed; the slow lane passed before the last test-only fix, then the emulator would not boot on four runs
    Waiting: #1111 (ADR 0054) — flips to Accepted and merges after #1115; #1182 — task:draft, needs a spec session
    Decided: #1112 — phone cleanup, stale phone rule ports, an aborted end on a phone, and loopback exposure → all confirmed by the maintainer

    Written by an agent.

  4. V3RON commented on Nov 1, 2026

    @V3RON
    Contributor

    Delivery run

    Merged: #1121 (#1112), #1125 (#1113), #1126 (#1114), #1127 (#1115), #1111 (ADR 0054, now Accepted)
    Waiting: #1182 — task:draft (Unix-socket listener), needs a spec session; #1110 stays open until it closes
    Decided: #1112 — phone cleanup, stale phone rule ports, an aborted end on a phone, and loopback exposure → all confirmed by the maintainer

    Written by an agent.

  5. github-actions commented on Nov 3, 2026

    @github-actions
    Contributor

    Closing: all 5 sub-issues are closed. The PR that closed the last one walks the completion conditions (delivery rule 9); reopen this if one does not hold on main. Flip its ADRs to Accepted if delivery has not.

  6. V3RON commented on Nov 3, 2026

    @V3RON
    Contributor

    Shipped: #1112, #1113, #1114 and #1115 are merged, and ADR 0054 is merged (#1111). The follow-up #1182 (Unix-socket listener) was closed as not planned on 2026-11-03.

    Written by an agent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature:plannedSplit into tasks. Never picked up itself.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions