Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5c90c70
feat(channels): add GitLab polling channel adapter
OrbitZore Jul 27, 2026
040f713
fix(channels/gitlab): persist cursor after each successful todo
OrbitZore Jul 27, 2026
f99b654
fix(channels/gitlab): persist cursor on every advancement including s…
OrbitZore Jul 27, 2026
b2d410e
fix(channels/gitlab): address review critical issues
OrbitZore Jul 28, 2026
a5db236
fix(channels/gitlab): address review suggestions
OrbitZore Jul 28, 2026
5927ecd
refactor(channels/gitlab): simplify to todo.body dispatch, add descri…
OrbitZore Jul 28, 2026
5ee59bd
docs(channels): add GitLab adapter documentation
OrbitZore Jul 28, 2026
728930d
fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }…
OrbitZore Jul 28, 2026
4a6046b
Merge remote-tracking branch 'upstream/main' into feat/gitlab-channel…
OrbitZore Jul 28, 2026
a16a8c4
chore: regenerate NOTICES.txt for new gitlab channel dependencies
OrbitZore Jul 28, 2026
ab2b228
fix(channels/gitlab): address review suggestions
OrbitZore Jul 28, 2026
fbc1e50
test(channels/gitlab): add mention tests, directly_addressed coverage…
OrbitZore Jul 28, 2026
9568588
fix(channels/gitlab): address review round 4
OrbitZore Jul 28, 2026
7cef052
fix(channels/gitlab): always force isMentioned=true, remove regex re-…
OrbitZore Jul 28, 2026
049a28d
fix(channels/gitlab): propagate fetchDescription errors for descripti…
OrbitZore Jul 28, 2026
200ad9e
perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDe…
OrbitZore Jul 28, 2026
fb7bf93
docs(channels/gitlab): clarify requireMention is bypassed, template i…
OrbitZore Jul 28, 2026
16aac38
Apply suggestions from code review
OrbitZore Jul 28, 2026
d70f6dd
fix(channels/gitlab): use todo ID cursor instead of timestamp to elim…
OrbitZore Jul 28, 2026
7f35c63
fix(channels/gitlab): harden first-poll drain, add ordering tests, fi…
OrbitZore Jul 28, 2026
9bd4463
fix(channels/gitlab): include dot in mention lookahead for GitLab use…
OrbitZore Jul 28, 2026
ba66ba7
docs(channels/gitlab): align docs with ID cursor and drain semantics
OrbitZore Jul 28, 2026
4545543
Apply suggestions from code review
OrbitZore Jul 29, 2026
009c15c
Merge remote-tracking branch 'upstream/main' into feat/gitlab-channel…
OrbitZore Jul 29, 2026
843590e
fix(channels/gitlab): align package version and channel-base dependen…
OrbitZore Jul 29, 2026
310e633
Merge branch 'feat/gitlab-channel-adapter' of github.com:OrbitZore/qw…
OrbitZore Jul 29, 2026
aa89a67
fix(channels/gitlab): regenerate lockfile to match package.json versions
OrbitZore Jul 29, 2026
409d6a8
test(channels/gitlab): add regression tests for first-poll drain hard…
OrbitZore Jul 29, 2026
96b77a4
docs(channels/gitlab): clarify groupPolicy must be "open" and add run…
OrbitZore Jul 29, 2026
bebd3ff
fix(channels/gitlab): correct xcase integrity hash in lockfile
OrbitZore Jul 29, 2026
6b23237
Merge branch 'main' into feat/gitlab-channel-adapter
wenshao Jul 29, 2026
d32549e
fix(channels/gitlab): correct requester-utils integrity hash in lockfile
OrbitZore Jul 29, 2026
b48dbe5
fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs
OrbitZore Jul 29, 2026
a586788
Merge branch 'main' into feat/gitlab-channel-adapter
wenshao Jul 29, 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
18 changes: 10 additions & 8 deletions docs/developers/daemon/15-channel-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,14 @@ Handles common cross-cutting concerns: sender gating (allowlist / denylist), gro

### Per-channel adapters

| Adapter | File | Transport | Notes |
| --------------- | --------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| DingTalk | `packages/channels/dingtalk/src/DingtalkAdapter.ts` | DingTalk Stream SDK WebSocket | Sends via `sessionWebhook` POST; media images downloaded via DT API, base64 in envelope. |
| WeChat (Weixin) | `packages/channels/weixin/src/WeixinAdapter.ts` | iLink Bot HTTP long-poll | Sends via proprietary `sendText` / `sendImage` API; typing indicators. |
| Telegram | `packages/channels/telegram/src/TelegramAdapter.ts` | Telegram Bot API long-poll (grammy) | Sends HTML chunks via `sendMessage`. |
| Feishu | `packages/channels/feishu/src/FeishuAdapter.ts` | Feishu/Lark Stream WebSocket (default) or HTTP webhook | Sends via Lark SDK as interactive cards; webhook mode requires `encryptKey` for HMAC signature verification. |
| GitHub | `packages/channels/github/src/GithubAdapter.ts` | GitHub Notifications API polling (`@octokit/rest`) | Extends `PollingChannelBase`; cursor-based comment window dedup; posts comments via Issues API. |
| Adapter | File | Transport | Notes |
| --------------- | --------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| DingTalk | `packages/channels/dingtalk/src/DingtalkAdapter.ts` | DingTalk Stream SDK WebSocket | Sends via `sessionWebhook` POST; media images downloaded via DT API, base64 in envelope. |
| WeChat (Weixin) | `packages/channels/weixin/src/WeixinAdapter.ts` | iLink Bot HTTP long-poll | Sends via proprietary `sendText` / `sendImage` API; typing indicators. |
| Telegram | `packages/channels/telegram/src/TelegramAdapter.ts` | Telegram Bot API long-poll (grammy) | Sends HTML chunks via `sendMessage`. |
| Feishu | `packages/channels/feishu/src/FeishuAdapter.ts` | Feishu/Lark Stream WebSocket (default) or HTTP webhook | Sends via Lark SDK as interactive cards; webhook mode requires `encryptKey` for HMAC signature verification. |
| GitHub | `packages/channels/github/src/GithubAdapter.ts` | GitHub Notifications API polling (`@octokit/rest`) | Extends `PollingChannelBase`; cursor-based comment window dedup; posts comments via Issues API. |
| GitLab | `packages/channels/gitlab/src/GitlabAdapter.ts` | GitLab Todos API polling (`@gitbeaker/rest`) | Extends `PollingChannelBase`; dispatches `todo.body` directly; `action_prompt_template` config drives event filtering and metadata rendering. |

Each adapter implements:

Expand All @@ -98,6 +99,7 @@ Each adapter implements:
| **Telegram** | Bot API long-poll | `from.id` (+ optional `chat.id` for groups) | Inline keyboard buttons | Same |
| **Feishu** | WebSocket stream / HTTP webhook | `sender.open_id` (+ optional `chat_id` for groups) | Interactive card buttons | Same |
| **GitHub** | Notifications API polling | Numeric `user.id` (immutable; login resolved at connect) | Error comment + re-mention | `senderPolicy: 'allowlist' \| 'open'` |
| **GitLab** | Todos API polling | `author.username` (lowercased) | Log + re-mention | `senderPolicy: 'allowlist' \| 'open'` |

> **Note:** The "Permission UX" column describes each platform's native affordance, but none is wired up yet — `AcpBridge.requestPermission` currently auto-approves every request (`packages/channels/base/src/AcpBridge.ts`), and `ChannelConfig.approvalMode` is declared but not yet read. Interactive approval is planned (Phase 5).

Expand Down Expand Up @@ -186,7 +188,7 @@ Adapter `connect()` failures are reported separately from worker lifecycle error

- `packages/channels/base/` — `ChannelBase`, `PollingChannelBase`, `DaemonChannelBridge`, `types.ts` (`ChannelConfig`, `Envelope`, `SessionScope`, `ChannelPlugin`).
- `packages/sdk-typescript/src/daemon/` — `DaemonSessionClient` and friends.
- Per-channel SDKs: `@dingtalk/stream` (DingTalk), proprietary iLink Bot HTTP (Weixin), `grammy` (Telegram), `@octokit/rest` (GitHub polling).
- Per-channel SDKs: `@dingtalk/stream` (DingTalk), proprietary iLink Bot HTTP (Weixin), `grammy` (Telegram), `@octokit/rest` (GitHub polling), `@gitbeaker/rest` (GitLab polling).

## Configuration

Expand Down
1 change: 1 addition & 0 deletions docs/users/features/channels/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export default {
feishu: 'Feishu',
qqbot: 'QQ Bot',
github: 'GitHub',
gitlab: 'GitLab',
plugins: 'Plugins',
};
171 changes: 171 additions & 0 deletions docs/users/features/channels/gitlab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# GitLab

This guide covers setting up a Qwen Code channel that monitors GitLab todos and responds to mentions on issues and merge requests.

## Prerequisites

- A GitLab account (or a dedicated bot account)
- A GitLab Personal Access Token with `read_api` and `api` scopes

## Creating a Token

1. Go to **Preferences → Access Tokens**
2. Create a token with these scopes:
- **read_api** — read todos and project data
- **api** — post notes (comments) on issues/MRs
3. Save the token securely as an environment variable

## Configuration

Add the channel to `~/.qwen/settings.json`:

```json
{
"channels": {
"my-gitlab": {
"type": "gitlab",
"token": "$GITLAB_TOKEN",
"pollInterval": 60000,
"senderPolicy": "open",
"sessionScope": "chat_thread",
"cwd": "/path/to/your/project",
"groupPolicy": "open",
"action_prompt_template": {
"mentioned": "Project: %project% | URL: %project_url% | Author: %author% | Type: %target_type% | IID: %iid% | Title: %title% | Description: %description% | TodoID: %todo_id%"
}
}
}
}
```

Set the token as an environment variable:

```bash
export GITLAB_TOKEN="glpat-your_token_here"
```

### Self-hosted GitLab

For self-hosted instances, set `baseUrl`:

```json
{
"baseUrl": "https://gitlab.example.com"
}
```

## Configuration Options

| Option | Default | Description |
| ------------------------ | ------------------------- | ---------------------------------------------------------- |
| `token` | (required) | PAT with `read_api` + `api` scopes |
| `pollInterval` | `60000` | Poll interval in ms |
| `baseUrl` | `https://gitlab.com` | GitLab instance URL |
| `action_prompt_template` | (required for processing) | Maps GitLab action names to metadata templates |
| `groupPolicy` | `"disabled"` | Must be `"open"`, or `"allowlist"` with the project listed |
| `senderPolicy` | `"allowlist"` | Who can trigger the bot |

## action_prompt_template

This field controls which todo actions are processed and how metadata is rendered. Only actions with a configured template are dispatched; all others are skipped and marked done.

```json
{
"action_prompt_template": {
"mentioned": "Project: %project% | Author: %author% | Title: %title%"
}
}
```

The `directly_addressed` action (comment starting with `@bot`) automatically falls back to the `mentioned` template if not explicitly configured.

### Available Action Keys

| Key | Trigger |
| --------------------- | ------------------------------------------------------------------------ |
| `mentioned` | Someone @mentions the bot in a comment or description (not at the start) |
| `directly_addressed` | A comment **starts with** `@bot` (falls back to `mentioned` template) |
| `assigned` | Someone assigns the bot to an issue/MR |
| `review_requested` | Someone requests the bot as a reviewer on an MR |
| `approval_required` | An MR requires the bot's approval (approval rules) |
| `marked` | Someone marks the bot's comment/issue/MR (star) |
| `build_failed` | A CI/CD pipeline fails on the bot's branch/MR |
| `unmergeable` | An MR the bot is involved with becomes unmergeable (conflicts) |
| `merge_train_removed` | An MR is removed from the merge train |

Only keys present in `action_prompt_template` are processed. Unconfigured actions are skipped and marked done silently.

### Template Variables

| Variable | Value |
| --------------- | --------------------------------- |
| `%project%` | Project path (e.g., `owner/repo`) |
| `%project_url%` | Full project URL |
| `%author%` | Todo author username |
| `%target_type%` | `Issue` or `MergeRequest` |
| `%iid%` | Issue/MR internal ID |
| `%title%` | Issue/MR title |
| `%description%` | Issue/MR description body |
| `%todo_id%` | GitLab todo ID |
| `%%` | Literal `%` (escape) |

Unknown variables are preserved as-is in the output.

### Prompt Assembly

The template renders into `envelope.metadata` (structured context). The triggering text (`todo.body` or description) goes into `envelope.text` (primary prompt). The base class assembles the final prompt sent to the agent:

```
[alice] please fix this bug

Project: owner/repo | URL: https://gitlab.com/owner/repo | Author: alice | Type: Issue | IID: 42 | Title: Test Issue | Description: ... | TodoID: 100
```

- Line 1: `[sender]` prefix + `envelope.text` (with `@bot` stripped)
- Line 3: `envelope.metadata` (rendered template, sanitized)

You do **not** need a `%body%` variable — the comment/description text is always the primary prompt content, and the template provides supplementary context below it.

## ⚠️ Security

On a **public project**, setting `senderPolicy: "open"` allows **any GitLab user** who @mentions the bot to submit prompts that drive the agent in your `cwd`.

Always use `senderPolicy: "allowlist"` with explicit `allowedUsers` on public projects.

## Mention Detection

The adapter always sets `isMentioned = true` on dispatched envelopes, because GitLab has already determined the mention when creating the todo. The `action_prompt_template` config is the real event filter — only actions with a configured template are processed. The `@bot` mention is stripped from the message text before dispatch via `stripBotMention`.

### ⚠️ groupPolicy Must Be "open" or "allowlist"

`groupPolicy` must be set to `"open"`, or `"allowlist"` with the project explicitly listed, for todos to be processed. The default value `"disabled"` drops all mentions: todos are marked done and the cursor advances, but no dispatch occurs. A rejection is logged (`preflight rejected reason=group_disabled`) but the todo is still consumed. If your bot is not responding to mentions, check that `groupPolicy` is not `"disabled"`.

## How It Works

The adapter uses GitLab's Todos API as the message source:

1. **Poll** `GET /todos?state=pending` for new todos
2. **First-poll drain**: if the cursor has never been initialized (`initialized: false`), all pending todos are marked done without dispatch and the cursor advances to the max todo ID. This prevents a backlog flood on first start.
3. **Clean up stale todos**: todos with `id <= cursor` are marked done (best-effort) to prevent them from being re-fetched on every poll
4. **Filter** by `id > cursor` and configured `action_prompt_template`
5. **Detect mention type** via `target_url` anchor:
- `#note_123` present → comment mention → text is `todo.body` (the comment)
- No anchor → description mention → text is the issue/MR description
6. **Dispatch** the envelope through `handleInbound` (requires `groupPolicy: "open"` or `"allowlist"` with the project listed)
7. **Advance cursor** and **mark todo done** (best-effort)

The cursor (`lastProcessedId`) advances regardless of dispatch success or failure. Failed dispatches post a ⚠️ error comment on the issue/MR and are not retried — the user can re-mention the bot to trigger a new todo.

## Known Limitations

- **First start skips existing pending todos.** The cursor initializes to `{ lastProcessedId: 0, initialized: false }` on first launch. On the first poll cycle, all pre-existing pending todos are marked done without dispatch (the `initialized` flag gates this one-time drain), preventing a backlog flood.
- The bot does not read prior conversation history — only the triggering content is processed.
- **Confidential (internal) notes:** If someone @mentions the bot in a confidential note, the todo body contains that internal text and the agent will process it. The bot's reply is always posted as a **public** note, potentially exposing internal discussion. GitLab's todo API does not expose note visibility, so the adapter cannot filter this. Avoid @mentioning the bot in confidential notes.
- Requires `read_api` + `api` PAT scopes. Group-level or project-level tokens work if they have these scopes.
- Todos for Epics, Designs, and Alerts are skipped (only Issues and MRs are processed).

## Starting the Channel

```bash
qwen channel start my-gitlab
```
Loading
Loading