Plan: Secretary Agent¶
Goal¶
A cheap, always-on bot that bridges Telegram → GitHub Projects → night agent. You send a message describing a feature, bug, or question; the secretary creates a ticket on the GitHub Projects board and queues it for the night agent. As the night agent works, it moves tickets through Backlog → In Progress → Done so the board stays current without manual upkeep.
Model¶
DeepSeek V3 via api.deepseek.com (direct API, no OpenRouter markup).
| Base URL | https://api.deepseek.com/v1 |
| Model | deepseek-chat (V3) |
| Pricing | ~$0.27/1M input · ~$1.10/1M output |
| Auth | DEEPSEEK_API_KEY (already in config.py) |
| SDK | openai Python SDK pointed at DeepSeek base URL |
At typical secretary usage (a few messages/day + lightweight intent parsing), cost is effectively zero.
Architecture¶
You
│ Telegram message
▼
secretary/bot.py (polling loop, always-on)
│
├── DeepSeek V3 ← intent classification + ticket body generation
│
├── GitHub Projects GraphQL API
│ gh issue create + gh project item-add → new Backlog ticket (never createProjectV2DraftIssue — creates drafts)
│ updateProjectV2ItemFieldValue → move ticket status
│ listProjectV2Items → status queries
│
└── Telegram reply ← confirmation / status summary
Night agent (reads GitHub Projects board, not BACKLOG.md)
│ on start: move ticket In Progress
│ on done: move ticket Done
└── secretary/github_projects.py (shared helper)
Intents¶
The secretary classifies each Telegram message into one of:
| Intent | Action |
|---|---|
new_feature |
Create Backlog ticket with title + description generated by DeepSeek |
new_bug |
Create Backlog ticket tagged as bug |
status |
Query board, reply with counts per column and last 5 Done items |
move_ticket |
Move named ticket to a specified status |
question |
Answer from project state (BACKLOG.md, recent git log) — no ticket created |
Components¶
secretary/bot.py¶
Main entry point. Long-polls Telegram using raw requests (no extra SDK needed — we already call
Telegram in notifications/telegram.py). Authorized by TELEGRAM_CHAT_ID — any message from a
different chat is silently ignored.
while True:
updates = telegram_get_updates(offset)
for update in updates:
if str(update.chat_id) != TELEGRAM_CHAT_ID:
continue
intent, params = classify(update.text) # DeepSeek call
result = dispatch(intent, params)
telegram_send(TELEGRAM_CHAT_ID, result)
sleep(SECRETARY_POLL_INTERVAL) # default 10s
secretary/github_projects.py¶
Thin wrapper around the GitHub Projects v2 GraphQL API. Shared by both the secretary bot and the night agent so neither duplicates the GraphQL logic.
Key functions:
- create_ticket(title, body, status="Backlog") -> item_id — creates a real GitHub issue then adds it to the project board (two-step: gh issue create + gh project item-add)
- move_ticket(item_id, status) — status one of Backlog | In Progress | In Review | Done
- list_tickets(status=None) -> list[dict]
- find_ticket(title_fragment) -> item_id | None
Uses GH_TOKEN env var (same token gh CLI uses — gh auth token to retrieve).
secretary/intent.py¶
DeepSeek call with a short system prompt. Returns structured JSON:
{
"intent": "new_feature",
"title": "Add earnings surprise heatmap to Overview",
"body": "Show EPS surprise % for all tickers as a colour-coded grid ...",
"priority": "medium"
}
System prompt keeps it minimal — one round-trip, no conversation history needed.
Night agent changes¶
The night agent currently reads BACKLOG.md. With this plan, it additionally:
- At session start: calls
list_tickets(status="Backlog")to pick the next feature - Before starting work: calls
move_ticket(item_id, "In Progress") - After QA passes: calls
move_ticket(item_id, "Done")
BACKLOG.md is kept as a human-readable mirror but is no longer the source of truth. The secretary
syncs it after each board change (optional, for git history).
Config additions (config.py)¶
SECRETARY_POLL_INTERVAL = 10 # seconds between Telegram polls
SECRETARY_MODEL = "deepseek-chat"
SECRETARY_BASE_URL = "https://api.deepseek.com/v1"
GH_PROJECT_ID = "PVT_kwHOBpDtN84BZY1n" # marketpulse project
GH_PROJECT_STATUS_FIELD = "PVTSSF_..." # field ID for Status column
GH_STATUS_OPTION_IDS = { # option IDs for each status value
"Backlog": "...",
"In Progress": "...",
"In Review": "...",
"Done": "98236657",
}
Field and option IDs are fetched once via GraphQL and hardcoded — they don't change.
Running¶
# Secretary bot (keep running — systemd, tmux, or pm2)
python secretary/bot.py
# Or via make
make secretary # adds a target that starts bot.py in background
The secretary is a separate process from main.py — it does not start with the scheduler.
Implementation Checklist¶
- [ ] Add
DEEPSEEK_API_KEYnote to.envdocs (already in config.py, just needs key) - [ ] Add
GH_TOKENto.env(used for Projects GraphQL writes) - [ ] Add
SECRETARY_*andGH_PROJECT_*constants toconfig.py - [ ] Fetch and record Status field ID + option IDs via GraphQL
- [ ] Write
secretary/github_projects.py - [ ] Write
secretary/intent.py(DeepSeek classification) - [ ] Write
secretary/bot.py(Telegram polling loop) - [ ] Update night agent (
scheduler/jobs.pyor night-flow skill) to call github_projects on start/done - [ ] Add
make secretarytarget toMakefile - [ ] Test end-to-end: send Telegram message → ticket appears on board → night agent moves it
Open Questions¶
GH_TOKENpermissions: needsprojectscope.gh auth tokenon this machine may already have it — verify withgh auth status.- Status field IDs: must be fetched via
projectV2GraphQL query before wiring up config. - BACKLOG.md sync: keep it as a mirror (secretary writes it after each board change) or deprecate it? Suggestion: keep for git history, but treat board as authoritative.
- Night agent trigger: does the secretary actively wake the night agent (e.g. via a signal file or API call), or does the agent just run on its fixed schedule and pick up whatever is in Backlog? Simpler to let the agent run on schedule — secretary just manages the board.