feat(TER-11): codex-termux_fork submodule + deepcli bridge scaffold - #8
Conversation
…ffold Add codex-termux/ project dir with codex-termux_fork as a shallow (--depth 1) submodule and a codex_bridge Python scaffold (doctor/build/ run/reconcile) plus README runbooks (Termux reconcile + MCP connect). Wiring/scaffolding only; deepcli<->Codex protocol translation is not yet implemented (documented in the refactor roadmap). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Proposed
|
| fork | why | note |
|---|---|---|
codex-termux_fork |
native codex base + deepcli bridge | ✅ this PR (codex-termux/) |
termux-mcp-server_fork |
on-device Termux/ADB MCP | see PR #7 integrations/termux-mcp/ |
phone-mcp-server_fork |
phone control MCP (node) | run locally |
mcp-android-ssh_fork |
Android SSH MCP (Rust) | russh 0.54.6 pulls yanked libcrux-ml-kem 0.0.3; bump needed |
refTemplates/ — scavenged as templates only → metadata selective sparse-checkout --depth 1, README + the listed subtree(s), nothing else:
| fork | sparse paths to keep |
|---|---|
AIstudioProxyAPI_fork / AIstudioProxyAPIClient_fork |
README*, launch_camoufox.py, api_utils/, browser_utils/ |
AIStudioProxy_fork (py) |
README*, src/aistudioproxy/ |
AIstudioProxyAPI_fork-node / AIStudioToAPI_fork / AIStudio2API_fork |
README*, server.*, auto_connect_* / ui/ |
aistudio-gemini-mcp_fork / gemini-cli-api-wrapper_fork |
README*, server.* / src/ |
ChapitoAI_fork |
README*, chapito/ (provider scaffold template) |
chAIt_fork |
README*, chait/ (PyQt6 WebEngine client) |
Gantt-Chart-Code_fork |
README*, src/ (Rust reference) |
deepcode-cli_fork / deepseek-mcp-server_fork |
README*, packages//src/ |
awesome-deepseek-integration_fork / awesome-deepseek-agent_fork / awesome-agent-harness_fork |
README* only (curated lists) |
Sparse recipe per template fork (identical in Termux):
git submodule add --depth 1 <url> refTemplates/<name>
git -C refTemplates/<name> sparse-checkout init --cone
git -C refTemplates/<name> sparse-checkout set <paths…>
# mark shallow=true in .gitmodulesOpen questions before I add these:
refTemplates/vs a different root name?- Confirm the per-fork sparse paths above (esp. the AI-Studio proxies — happy to trim to just the launcher + core module).
- Should the MCP-server forks live under
integrations/here, or stay purely in PR feat: Termux MCP integration (pull-to-local + Devin custom MCP configs) #7's layout?
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Observation from Termux smoke (auth gate)
That is expected given the current scaffold ( Follow-up issueOpened TER-12 — Prioritize deepcli as primary driver for custom Codex (rename: DeepForge) — skip OpenAI ChatGPT auth gate. Working product name proposal for our eventual native agent (distinct from stock OpenAI Codex):
Suggested direction for the next iteration of the bridge:
Happy to take the first cut on a |
| run = subparsers.add_parser("run", help="run the resolved Codex binary") | ||
| run.add_argument("args", nargs=argparse.REMAINDER) |
There was a problem hiding this comment.
🟡 Passing any option flag to the Codex runner makes the command fail instead of running Codex
Arguments meant for Codex are collected with a trailing catch-all (run.add_argument("args", nargs=argparse.REMAINDER) at codex-termux/bridge/codex_bridge/cli.py:167) that does not capture leading dash-options, so the documented run --help prints the bridge's own help and any other flag aborts with "unrecognized arguments".
Impact: Users cannot pass options through to the Codex binary; the documented run commands fail or do the wrong thing.
Why REMAINDER does not capture leading options in a subparser
Verified locally: main(['run','--help']) prints the run subparser help and exits 0 (never reaching _run at codex-termux/bridge/codex_bridge/cli.py:99), and parse_args(['run','--foo','bar']) errors with unrecognized arguments: --foo because a subparser hands unconsumed option-looking tokens back to the top-level parser. The same affects make run ARGS="--help" (codex-termux/Makefile:18-19) and both README runbooks (codex-termux/README.md:39, codex-termux/README.md:49).
Using nargs=argparse.REMAINDER only works reliably on the top-level parser; a robust fix is parser.parse_known_args() plus manual splitting, or requiring -- before pass-through args while also disabling add_help on the run subparser.
Prompt for agents
The `run` subcommand in codex-termux/bridge/codex_bridge/cli.py is meant to forward all remaining arguments to the resolved Codex binary, but `nargs=argparse.REMAINDER` on a subparser does not capture arguments that start with a dash: `run --help` is intercepted by the subparser's own -h/--help, and `run --foo` fails top-level parsing with 'unrecognized arguments'. Consider disabling help on the `run` subparser (add_help=False) and/or using parse_known_args and splitting argv manually at the 'run' token so everything after it is forwarded verbatim. Update codex-termux/README.md examples if a `--` separator convention is adopted.
Was this helpful? React with 👍 or 👎 to provide feedback.
| BRIDGE_DIR = Path(__file__).resolve().parents[2] | ||
| REPO_ROOT = BRIDGE_DIR.parent | ||
| CODEX_ROOT = BRIDGE_DIR / "codex-termux_fork" |
There was a problem hiding this comment.
📝 Info: BRIDGE_DIR actually points at codex-termux/, not the bridge directory
Path(__file__).resolve().parents[2] resolves to codex-termux/ (parents[0]=codex_bridge, [1]=bridge, [2]=codex-termux), so BRIDGE_DIR is misnamed while REPO_ROOT and CODEX_ROOT derived from it are correct. The Makefile separately defines BRIDGE_DIR := $(CURDIR)/bridge (codex-termux/Makefile:4), meaning the same name refers to two different directories across the codebase — likely to cause confusion for future edits.
Was this helpful? React with 👍 or 👎 to provide feedback.
| print(f"Reconcile aborted: {error}", file=sys.stderr) | ||
| return 1 | ||
| pointers = index["pointers"] | ||
| existing_sids = { | ||
| pointer.get("sid") | ||
| for pointer in pointers | ||
| if isinstance(pointer, dict) and pointer.get("sid") | ||
| } | ||
| added = 0 | ||
| for session_file in sorted(store.glob("**/*.json")): | ||
| sid = session_file.stem | ||
| if sid in existing_sids: | ||
| continue | ||
| content_hash = hashlib.sha256(session_file.read_bytes()).hexdigest() |
There was a problem hiding this comment.
📝 Info: Session ids are assumed unique across accounts
Sessions live under ~/.deepcli/session_store/{account}/{sid}.json (confirmed at deepcli/deepcli/core.py), and reconcile keys purely on session_file.stem. If two accounts ever produce the same sid, only the first (sorted) file is recorded and the second is silently skipped with no diagnostic. Keying on account+sid, or at least warning on collision, would make this safer.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def _load_index(index_path: Path) -> Dict[str, Any]: | ||
| if not index_path.is_file(): | ||
| return {"pointers": []} | ||
| try: | ||
| data = json.loads(index_path.read_text()) | ||
| except (OSError, json.JSONDecodeError) as error: | ||
| raise _IndexUnreadable(f"cannot read {index_path}: {error}") from error | ||
| if not isinstance(data, dict): | ||
| raise _IndexUnreadable(f"{index_path} must contain a JSON object") | ||
| pointers = data.get("pointers") | ||
| if not isinstance(pointers, list): | ||
| raise _IndexUnreadable(f"{index_path} must contain a list at `pointers`") | ||
| return data | ||
|
|
||
|
|
||
| def _reconcile() -> int: | ||
| store = paths.deepcli_store() | ||
| index_path = paths.codex_index() | ||
| store.mkdir(parents=True, exist_ok=True) | ||
| index_path.parent.mkdir(parents=True, exist_ok=True) | ||
| try: | ||
| index = _load_index(index_path) | ||
| except _IndexUnreadable as error: | ||
| print(f"Reconcile aborted: {error}", file=sys.stderr) | ||
| return 1 | ||
| pointers = index["pointers"] | ||
| existing_sids = { | ||
| pointer.get("sid") | ||
| for pointer in pointers | ||
| if isinstance(pointer, dict) and pointer.get("sid") | ||
| } | ||
| added = 0 | ||
| for session_file in sorted(store.glob("**/*.json")): | ||
| sid = session_file.stem | ||
| if sid in existing_sids: | ||
| continue | ||
| content_hash = hashlib.sha256(session_file.read_bytes()).hexdigest() | ||
| pointers.append( | ||
| { | ||
| "sid": sid, | ||
| "ch": content_hash, | ||
| "path": str(session_file), | ||
| "source": "deepcli", | ||
| } | ||
| ) | ||
| existing_sids.add(sid) | ||
| added += 1 | ||
| index_path.write_text(json.dumps(index, indent=2) + "\n") |
There was a problem hiding this comment.
📝 Info: Index rewrite is not atomic and only guards JSON/OS errors
index_path.write_text(...) overwrites the existing index in place; an interrupted write (battery kill on a phone is realistic for Termux) leaves a truncated codex_index.json, which the next run refuses to merge into (_IndexUnreadable). A write-to-temp + os.replace would preserve the abort-rather-than-clobber guarantee the README advertises. Relatedly, _load_index catches OSError/JSONDecodeError but not UnicodeDecodeError from read_text, so a binary/mis-encoded index raises a raw traceback instead of the clean "Reconcile aborted" message.
Was this helpful? React with 👍 or 👎 to provide feedback.
| str(binary) if binary else "build with: make build", | ||
| ) | ||
| ) | ||
| checks.append( | ||
| _print_status( | ||
| "deepcli", |
There was a problem hiding this comment.
📝 Info: deepcli importability check is executed twice and mutates sys.path
_deepcli_importable() is invoked twice in the same status line (once for the boolean, once for the detail string), importing the package and pushing/popping sys.path twice. The sys.path.pop(0) in the finally assumes the imported package did not itself insert entries at index 0; using sys.path.remove(package_root) or restoring a saved copy would be more robust. Caching the result in a local also avoids the duplicate import.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def codex_binary() -> Optional[Path]: | ||
| configured = configured_binary() | ||
| if configured: | ||
| return configured | ||
| for profile in ("release", "debug"): | ||
| candidate = CODEX_ROOT / "codex-rs" / "target" / profile / "codex" | ||
| if candidate.is_file(): | ||
| return candidate |
There was a problem hiding this comment.
📝 Info: An explicit TERMUX_CODEX_BIN that no longer exists blocks the built binary
codex_binary() returns the configured path without checking existence, so a stale TERMUX_CODEX_BIN short-circuits the target/release, target/debug, and command -v codex fallbacks. doctor then reports MISSING and run refuses to start even though a freshly built binary is present. If the intent is 'override when usable', consider validating is_file() before returning the configured path.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def _build() -> int: | ||
| if not (paths.CODEX_ROOT / "codex-rs" / "Cargo.toml").is_file(): | ||
| print("MISSING submodule: run `make init` first", file=sys.stderr) | ||
| return 1 | ||
| result = subprocess.run( | ||
| ["cargo", "build", "-p", "codex-cli"], | ||
| cwd=paths.CODEX_ROOT / "codex-rs", | ||
| check=False, | ||
| ) |
There was a problem hiding this comment.
📝 Info: Build target crashes with a traceback if cargo is absent
_build checks for the submodule Cargo.toml but not for the cargo executable; on a Termux device without Rust installed this raises FileNotFoundError from subprocess.run rather than printing a MISSING-style message consistent with the rest of the tool.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
sha: f5d428d @jules opsSweep (heyVern lane) — high-perf unattended advance. PR #8 · Instructions
Monikers: docs/ops/AGENT-MONIKERS.md · Read AGENTS.md. |
|
Closing as superseded: diffed this PR's |
Summary
First TER-11 wiring: shallow submodule
codex-termux_forkplus a smallcodex_bridgescaffold (doctor/build/run/reconcile).Status: 🟡 Conditional — scaffold only
Disposition: Honest scaffolding; not full deepcli↔Codex protocol. Prefer retarget to
master-stagingbefore merge.Base:
master(should move tomaster-staging)Implements: TER-11 (scaffold)
Changes
codex-termux/+ shallow submodule →timerloggedout-spec/codex-termux_forkTERMUX_CODEX_BIN, deepcli import check, SSOT path notesreconcilemerges deepcli session pointers into index conservatively (preserve non-deepcli)Non-goals
Validation
py_compilebridge modules;make -n;git diff --checkdoctor/ temp-HOMEreconcileas reported in original bodyAgent notes
grok-archw1zdevin— next body tweak may be Devin’s to satisfy anti-monopoly roster