Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0052a98
ops(deepcli): add dual-agent Termux bridge CI smoke
timerloggedout-spec Oct 1, 2026
7c59548
ci(deepcli): run bridge smoke on pull requests
timerloggedout-spec Oct 1, 2026
47e4328
ci(deepcli): preserve bridge admission evidence on gated runs
timerloggedout-spec Oct 1, 2026
a38df4e
feat(deepcli): add role-aware continuous prompt contracts
timerloggedout-spec Oct 4, 2026
c056df8
feat(deepcli): add role-aware continuous prompt contracts
timerloggedout-spec Oct 4, 2026
383e0fb
feat(deepcli): add role-aware continuous prompt contracts
timerloggedout-spec Oct 4, 2026
7ffae97
feat(deepcli): activate role-aware prompt contract in deepagent
timerloggedout-spec Oct 4, 2026
33a00f9
feat(deepcli): activate role-aware prompt contract in agent
timerloggedout-spec Oct 4, 2026
297e1d4
fix(deepcli): load prompt system from script directory
timerloggedout-spec Oct 4, 2026
f3e05ee
feat(deepcli): add auditable DeepAgent task runner
timerloggedout-spec Oct 4, 2026
e8d87df
feat(deepcli): dispatch assigned continuous task through bridge
timerloggedout-spec Oct 4, 2026
74a7578
fix(deepcli): expand runner SSH identity path correctly
timerloggedout-spec Oct 4, 2026
22661e9
feat(deepcli): require explicit verified task finish
timerloggedout-spec Oct 4, 2026
91d25e6
fix(deepcli): enforce explicit task receipt in bridge dispatch
timerloggedout-spec Oct 4, 2026
190a29a
fix(deepcli): correct bridge subprocess identity handling
timerloggedout-spec Oct 4, 2026
e63d9f8
fix(deepcli): import os for bridge subprocesses
timerloggedout-spec Oct 4, 2026
4ef62cf
feat(deepcli): add continuous scheduled agent continuation
timerloggedout-spec Oct 4, 2026
ecc2754
fix(deepcli): import os for smoke SSH path
timerloggedout-spec Oct 4, 2026
985f9a6
ci(deepcli): make DeepAgent execution the verification gate
timerloggedout-spec Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions .github/workflows/deepcli-agent-bridge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: DeepCLI Agent Bridge Smoke

on:
workflow_dispatch:
schedule:
- cron: "*/30 * * * *"
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [master]
paths:
- "deepcli/agent.py"
- "deepcli/deepagent.py"
- "deepcli/session_manager.py"
- "deepcli/core.py"
- "ops/termux-bridge/current.json"
- "scripts/termux/**"
- ".github/workflows/deepcli-agent-bridge.yml"

permissions:
contents: read
actions: read

concurrency:
group: deepcli-agent-bridge
cancel-in-progress: true

jobs:
bridge-smoke:
name: Termux bridge + dual agent smoke
runs-on: ubuntu-latest
timeout-minutes: 12
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
fetch-depth: 1
persist-credentials: false

- name: Read and validate live bridge manifest
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh api "repos/${GITHUB_REPOSITORY}/contents/ops/termux-bridge/current.json?ref=master" \
--jq '.content' | tr -d '\n' | base64 -d > bridge.json
python - <<'PY'
import json
from datetime import datetime, timezone

d = json.load(open("bridge.json", encoding="utf-8"))
with open("deepcli-agent-bridge-receipt.json", "w", encoding="utf-8") as out:
json.dump({
"schema_version": 1,
"run_id": int("${{ github.run_id }}"),
"run_attempt": int("${{ github.run_attempt }}"),
"sha": "${{ github.sha }}",
"ref": "${{ github.ref }}",
"bridge": {
"status": d.get("status"),
"endpoint": d.get("endpoint"),
"port": d.get("port"),
"ssh_user": d.get("ssh_user"),
"expires_at": d.get("expires_at"),
"stale_reason": d.get("stale_reason"),
},
"checks": [],
}, out, indent=2, sort_keys=True)
required = ("endpoint", "port", "ssh_user", "expires_at")
if d.get("status") != "active":
raise SystemExit(f"bridge status is {d.get('status')!r}; publisher must refresh it")
missing = [k for k in required if not d.get(k)]
if missing:
raise SystemExit(f"bridge manifest incomplete: {missing}")
expires = datetime.fromisoformat(d["expires_at"].replace("Z", "+00:00"))
if expires <= datetime.now(timezone.utc):
raise SystemExit("bridge endpoint is expired")
if not (1 <= int(d["port"]) <= 65535):
raise SystemExit("bridge port out of range")
print(f"bridge={d['ssh_user']}@{d['endpoint']}:{d['port']}")
PY

- name: Install pinned SSH identity
env:
SSH_KEY: ${{ secrets.TERMUX_MCP_SSH_PRIVATE_KEY }}
KNOWN_HOSTS: ${{ secrets.TERMUX_MCP_KNOWN_HOSTS }}
run: |
set -euo pipefail
test -n "$SSH_KEY" || { echo "::error::TERMUX_MCP_SSH_PRIVATE_KEY is missing"; exit 1; }
test -n "$KNOWN_HOSTS" || { echo "::error::TERMUX_MCP_KNOWN_HOSTS is missing"; exit 1; }
install -d -m 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/termux_mcp
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/termux_mcp ~/.ssh/known_hosts

- name: Sync DeepAgent payload to Termux
run: |
set -euo pipefail
python - <<'PY'
import json, subprocess, os
d=json.load(open("bridge.json", encoding="utf-8"))
base=["scp","-i",os.path.expanduser("~/.ssh/termux_mcp"),"-o","IdentitiesOnly=yes","-o","StrictHostKeyChecking=yes","-o","ConnectTimeout=15","-P",str(d["port"])]
target=f"{d['ssh_user']}@{d['endpoint']}:~/deepcli/"
files=["deepcli/prompt_system.py","deepcli/roles.json","deepcli/deepagent.py","deepcli/agent.py","scripts/termux/run-deepagent-task.sh"]
r=subprocess.run(base+files+[target],text=True,capture_output=True)
if r.returncode:
print(r.stdout); print(r.stderr)
raise SystemExit(r.returncode)
PY

- name: Dispatch assigned DeepAgent task
env:
DEEPCLI_TASK_ID: deepagent-ci-continuation-001
DEEPCLI_ROLE: engineer
DEEPCLI_TRANSPORT: auto
DEEPCLI_TASK: >-
Inspect and operationalize the DeepCLI agent execution path now. Verify the
role-aware prompt system, task identity, checkpoint/resume behavior, and the
direct and HTTP agent paths. Make only safe, task-scoped changes. Run focused
verification. Do not claim success unless the objective is evidenced. If the
transport is unavailable, classify that as access/admission failure and preserve
the exact evidence for the next continuation.
run: |
set -euo pipefail
python - <<'PY'
import json, shlex, subprocess, sys, os
d=json.load(open("bridge.json", encoding="utf-8"))
ssh=["ssh","-i",os.path.expanduser("~/.ssh/termux_mcp"),"-o","IdentitiesOnly=yes","-o","StrictHostKeyChecking=yes","-o","ConnectTimeout=15","-p",str(d["port"]),f"{d['ssh_user']}@{d['endpoint']}"]
task="Inspect and operationalize the DeepCLI agent execution path now. Verify the role-aware prompt system, task identity, checkpoint/resume behavior, and the direct and HTTP agent paths. Make only safe, task-scoped changes. Run focused verification. Do not claim success unless the objective is evidenced. If the transport is unavailable, classify that as access/admission failure and preserve the exact evidence for the next continuation."
remote="DEEPCLI_TASK_ID=deepagent-ci-continuation-001 DEEPCLI_ROLE=engineer DEEPCLI_TRANSPORT=auto DEEPCLI_TASK="+shlex.quote(task)+" bash -lc 'DEEPCLI_TASK_ID=deepagent-ci-continuation-001 DEEPCLI_ROLE=engineer DEEPCLI_TRANSPORT=auto DEEPCLI_TASK="+shlex.quote(task)+" bash ~/deepcli/run-deepagent-task.sh'"
r=subprocess.run(ssh+[remote],text=True,capture_output=True,timeout=600)
open("deepagent-task-output.txt","w").write(r.stdout[-12000:])
open("deepagent-task-error.txt","w").write(r.stderr[-6000:])
print(r.stdout[-4000:]); print(r.stderr[-2000:],file=sys.stderr)
if r.returncode:
raise SystemExit(r.returncode)
PY

# DeepAgent task execution is the verification path. No secondary smoke gate is required.\n\n - name: Upload DeepAgent task evidence
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.2
with:
name: deepagent-task-evidence-${{ github.run_id }}-${{ github.run_attempt }}
path: |
deepagent-task-output.txt
deepagent-task-error.txt
if-no-files-found: warn
retention-days: 14

- name: Upload immutable smoke receipt
if: always()
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.2
with:
name: deepcli-agent-bridge-receipt-${{ github.run_id }}-${{ github.run_attempt }}
path: deepcli-agent-bridge-receipt.json
if-no-files-found: error
retention-days: 14
17 changes: 14 additions & 3 deletions deepcli/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
HUB = os.environ.get("DSH_HUB", "http://127.0.0.1:8800")
TOK = open(os.path.expanduser("~/.deepcli/hub.token")).read().strip()
import session_store
from prompt_system import build_system_prompt

MAX_STEPS = int(os.environ.get("AGENT_MAX_STEPS", "16"))

Expand Down Expand Up @@ -358,9 +359,10 @@ def _agent_notify(kind: str, title: str, content: str, priority: str = "default"
pass


def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False):
def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False, role=None, task_id=None, transport=None):
print(f"\n▶ task: {task}\n")
msgs = [{"role":"user","content":task}]
print(f" [role] {role or os.environ.get("DEEPCLI_ROLE", "engineer")} [task_id] {task_id or os.environ.get("DEEPCLI_TASK_ID", "unassigned")} [transport] {transport or os.environ.get("DEEPCLI_TRANSPORT", "auto")}")
msgs = [{"role":"system","content":build_system_prompt(task, role=role, task_id=task_id, transport=transport)}, {"role":"user","content":task}]
for step in range(MAX_STEPS):
print(f"── step {step+1}/{MAX_STEPS} ──")
if step > 0:
Expand Down Expand Up @@ -396,8 +398,17 @@ def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False
if __name__ == "__main__":
argv = sys.argv[1:]
dry = False
role = os.environ.get("DEEPCLI_ROLE", "engineer")
task_id = os.environ.get("DEEPCLI_TASK_ID")
transport = os.environ.get("DEEPCLI_TRANSPORT", "auto")
if argv and argv[0] == "--dry-run":
dry = True; argv = argv[1:]
if "--role" in argv:
i=argv.index("--role"); role=argv[i+1]; del argv[i:i+2]
if "--task-id" in argv:
i=argv.index("--task-id"); task_id=argv[i+1]; del argv[i:i+2]
if "--transport" in argv:
i=argv.index("--transport"); transport=argv[i+1]; del argv[i:i+2]
if not argv:
print("usage: agent.py [--dry-run] \"<task>\""); sys.exit(2)
loop(" ".join(argv), dry_run=dry)
loop(" ".join(argv), dry_run=dry, role=role, task_id=task_id, transport=transport)
16 changes: 13 additions & 3 deletions deepcli/deepagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
sys.path.insert(0, str(HOME))

from deepcli.core import get_token, create_session, chat_completion # noqa
from deepcli.prompt_system import build_system_prompt # noqa
from src.gh_broker import GhBroker # noqa

# Hindsight memory (optional, env-gated)
Expand Down Expand Up @@ -918,9 +919,10 @@ def _autosnapshot(reason: str = "auto"):
pass


def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False):
def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False, role=None, task_id=None, transport=None):
"""Loop until finish OR no-progress detected. Ceiling is safety, not policy."""
print(f"\n▶ task: {task}\n")
print(f" [role] {role or os.environ.get("DEEPCLI_ROLE", "engineer")} [task_id] {task_id or os.environ.get("DEEPCLI_TASK_ID", "unassigned")} [transport] {transport or os.environ.get("DEEPCLI_TRANSPORT", "auto")}")
_t0 = time.time()
key = session_store.task_key(task, task_path)
_notify("run", "🤖 Agent starting",
Expand Down Expand Up @@ -961,13 +963,15 @@ def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False
# the network-interrupt checkpoint exists, else start fresh.
if resumed_state:
msgs = resumed_state["msgs"]
if not msgs or msgs[0].get("role") != "system":
msgs.insert(0, {"role":"system","content":build_system_prompt(task, role=role, task_id=task_id, transport=transport)})
sid = resumed_state.get("sid") or sid
parent_id = resumed_state.get("parent_id")
seen_sigs = resumed_state.get("seen_sigs") or []
step_offset = resumed_state.get("step", 0)
no_progress = 0
else:
msgs = [{"role":"user","content":task}]
msgs = [{"role":"system","content":build_system_prompt(task, role=role, task_id=task_id, transport=transport)}, {"role":"user","content":task}]
parent_id = None
seen_sigs = []
step_offset = 0
Expand Down Expand Up @@ -1070,6 +1074,9 @@ def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False
"step": step + 1,
"task": task[:400],
"task_path": str(task_path) if task_path else None,
"task_id": task_id or os.environ.get("DEEPCLI_TASK_ID"),
"role": role or os.environ.get("DEEPCLI_ROLE", "engineer"),
"transport": transport or os.environ.get("DEEPCLI_TRANSPORT", "auto"),
"saved_at": time.time(),
})
except Exception as _e:
Expand All @@ -1093,6 +1100,9 @@ def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False
argv = sys.argv[1:]
dry = False
fresh = False
role = os.environ.get("DEEPCLI_ROLE", "engineer")
task_id = os.environ.get("DEEPCLI_TASK_ID")
transport = os.environ.get("DEEPCLI_TRANSPORT", "auto")
task_path = None
if "--dry-run" in argv:
dry = True; argv.remove("--dry-run")
Expand All @@ -1106,4 +1116,4 @@ def loop(task, dry_run=False, model="deepseek-chat", task_path=None, fresh=False
if not argv:
print("usage: deepagent.py [--dry-run] [--fresh] [--task-file P] \"<task>\"")
sys.exit(2)
loop(" ".join(argv), dry_run=dry, task_path=task_path, fresh=fresh)
loop(" ".join(argv), dry_run=dry, task_path=task_path, fresh=fresh, role=role, task_id=task_id, transport=transport)
73 changes: 73 additions & 0 deletions deepcli/prompt_system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Role-aware prompt contract for DeepCLI agents."""
from __future__ import annotations
import json, os
from pathlib import Path

ROOT = Path(__file__).resolve().parent
ROLE_FILE = ROOT / "roles.json"
DEFAULT_ROLE = os.environ.get("DEEPCLI_ROLE", "engineer")
DEFAULT_TRANSPORT = os.environ.get("DEEPCLI_TRANSPORT", "auto")

def _roles():
try:
return json.loads(ROLE_FILE.read_text(encoding="utf-8"))
except Exception:
return {}

def build_system_prompt(task: str, *, role: str | None = None,
task_id: str | None = None,
transport: str | None = None) -> str:
role = role or DEFAULT_ROLE
transport = transport or DEFAULT_TRANSPORT
cfg = _roles().get(role) or _roles().get("engineer", {})
capabilities = "\n".join(f"- {x}" for x in cfg.get("capabilities", []))
outputs = "\n".join(f"- {x}" for x in cfg.get("completion_evidence", []))
return f"""You are DeepAgent operating as the **{role}** role.

ROLE MISSION:
{cfg.get("mission", "Complete the assigned engineering task safely and verifiably.")}

SPECIALIZED CAPABILITIES:
{capabilities}

TASK:
{task}

TASK ID:
{task_id or "unassigned"}

TRANSPORT:
{transport}

OPERATING CONTRACT:
1. Inspect before editing. Prefer the smallest safe change that solves the task.
2. Use available tools and repository evidence; never invent runtime state.
3. Keep work bounded, resumable, and attributable to this task.
4. For multi-file or load-bearing changes, use an isolated worktree/branch and produce a PR.
5. Run the narrowest meaningful tests first, then broader checks when practical.
6. Distinguish code, test, provider, network/routing, and access/admission failures.
7. Never claim completion merely because work was dispatched, queued, or committed.
8. Before finish, verify the requested objective and report concrete evidence:
{outputs}
9. If blocked, preserve the exact blocker, evidence, and next action for a resumed worker.
10. Continuous mode means useful bounded continuation across resumptions; safety ceilings,
duplicate-call guards, and verification gates remain mandatory.

COMPLETION FORMAT:
When genuinely complete, call finish with:
- task id / objective
- files or external state changed
- verification commands/checks
- observed result
- remaining caveats, if any

ROLE-SPECIFIC PRIORITY:
{cfg.get("priority", "Correctness and evidence before speed.")}

Do not optimize for activity. Optimize for **verified useful change**.
"""

def load_role(role: str) -> dict:
data = _roles()
return data.get(role, data.get("engineer", {}))
Loading
Loading