Skip to content
Open
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
237ded0
to #10366 [feat] Show DingTalk dynamic lifecycle tags
BenGuanRan Aug 29, 2026
e1785c6
to #10366 [feat] Show lifecycle phases on DingTalk status cards
BenGuanRan Aug 31, 2026
5bc6d53
to #10366 [feat] Show lifecycle phases in DingTalk card body
BenGuanRan Aug 31, 2026
e88cf1d
to #10366 [feat] Harden DingTalk lifecycle presentation
BenGuanRan Aug 31, 2026
815740d
Merge remote-tracking branch 'origin/main' into feat/dingtalk-dynamic…
BenGuanRan Aug 31, 2026
06dbb46
to #10366 [bugfix] Close DingTalk lifecycle delivery races
BenGuanRan Aug 31, 2026
a476cbd
Merge remote-tracking branch 'origin/main' into feat/dingtalk-dynamic…
BenGuanRan Aug 31, 2026
cbeb1fa
to #10366 [bugfix] Preserve phases across reaction drain handoff
BenGuanRan Aug 31, 2026
773b408
feat(dingtalk): refine tool lifecycle phases
BenGuanRan Sep 1, 2026
a34ae43
test(dingtalk): cover granular lifecycle phases
BenGuanRan Sep 1, 2026
0d9eaa4
fix(dingtalk): preserve lifecycle phases across partial updates
BenGuanRan Sep 2, 2026
e017be3
Merge remote-tracking branch 'origin/main' into review/dingtalk-granu…
BenGuanRan Sep 2, 2026
d4280d6
Merge remote-tracking branch 'origin/main' into review/dingtalk-granu…
BenGuanRan Sep 3, 2026
b12cecb
Merge remote-tracking branch 'origin/main' into review/dingtalk-granu…
BenGuanRan Sep 3, 2026
448a469
fix(dingtalk): secure lifecycle phase updates
BenGuanRan Sep 3, 2026
e28a085
Merge remote-tracking branch 'origin/main' into review/dingtalk-granu…
BenGuanRan Sep 3, 2026
f6f9ba9
Merge remote-tracking branch 'origin/main' into review/dingtalk-granu…
BenGuanRan Sep 3, 2026
352cd69
test(cli): remove duplicate folder trust mock
BenGuanRan Sep 3, 2026
fa8e0a7
fix(channels): clear terminal tool kind cache
BenGuanRan Sep 3, 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
52 changes: 52 additions & 0 deletions docs/design/dingtalk-dynamic-lifecycle-tags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# DingTalk dynamic lifecycle tags

## Goal

Expose agent progress consistently on both the inbound DingTalk message and the interactive response card without changing the card template or exposing raw tool input, output, or reasoning.

## Lifecycle

- Start with two tags: `👀` and `🤔 Thinking`.
- Keep `👀` fixed while replacing only the status tag.
- Map tool events to `📖 Reading`, `🔎 Searching`, `🖥️ Running`, `🛠️ Editing`, `🛠️ Working`, or `⚠️ Retrying`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-11: The Lifecycle summary in this design doc enumerates only 6 tool phases while the same document's ACP projection contract table defines 10 — omitting 🗑️ Deleting, 📦 Moving, 🤔 Thinking, 🌐 Fetching, and 🔄 Switching mode. Both plan docs designate this file 'the final contract', so the stale summary is not inert: a maintainer working from the Lifecycle section — writing tag-cleanup coverage, auditing which status tags can appear on an inbound message, or projecting the lifecycle to another channel — will enumerate the 6 listed phases and conclude delete/move/think/fetch/switch_mode events collapse into 🛠️ Working, while the shipped toolPresentationPhase attaches 5 distinct tags: wrong expectations about observable UI state and incomplete cleanup/test lists.

Witness:

not run — textual self-contradiction settled by direct read of both sections at HEAD; no runnable oracle exists.

Suggested fix: Make the bullet match the contract table: '- Map tool events to the phases in the projection contract table below (📖 Reading, 🔎 Searching, 🖥️ Running, 🛠️ Editing, 🗑️ Deleting, 📦 Moving, 🤔 Thinking, 🌐 Fetching, 🔄 Switching mode, 🛠️ Working, or ⚠️ Retrying).'

中文说明

[Suggestion] 该设计文档的 Lifecycle 摘要只列出了 6 个工具阶段,而同一文档的 ACP 投影契约表定义了 10 个——遗漏了 🗑️ Deleting📦 Moving🤔 Thinking🌐 Fetching🔄 Switching mode。两份计划文档都将该文件指定为'最终契约',因此这份过时的摘要并非无害:从 Lifecycle 一节开展工作的维护者——编写标签清理覆盖、审计入站消息可能出现哪些状态标签、或将生命周期投影到其他频道——会枚举这 6 个阶段并得出 delete/move/think/fetch/switch_mode 事件归并为 🛠️ Working 的结论,而已交付的 toolPresentationPhase 会挂上 5 个不同的标签:对可观察 UI 状态的错误预期,以及不完整的清理/测试清单。

修复建议:使该条目与契约表一致:'- 将工具事件映射到下方投影契约表中的阶段(📖 Reading🔎 Searching🖥️ Running🛠️ Editing🗑️ Deleting📦 Moving🤔 Thinking🌐 Fetching🔄 Switching mode🛠️ Working⚠️ Retrying)。'

— qwen3.8-max via Qwen Code /review (v0.22.3)

- Map response text to `✍️ Replying`.
- On a terminal event, recall both transient tags before adding exactly one of `✅ Done`, `❌ Failed`, or `⏹️ Stopped`.

Reaction operations for one inbound message use a desired-state drain. A newer phase overwrites any pending phase, and a terminal event preempts every phase that has not reached DingTalk yet. An already in-flight API request cannot be cancelled, so the drain re-reads desired state after each response and removes any obsolete tag before settling. If a status recall fails, the replacement is skipped to avoid stacking contradictory statuses.

The same lifecycle mapping drives a replaceable first line in the interactive response card body. A newly created card starts at `🤔 Thinking`; tool activity replaces that line with the mapped phase before any response text exists, and the first response chunk changes it to `✍️ Replying`. While a response streams, its content appears below that phase line. Duplicate phase events are coalesced.

## ACP tool-kind projection contract

| ACP kind | Phase | English | Chinese |
| ------------------ | ----------- | ------------------- | --------------- |
| `read` | `reading` | `📖 Reading` | `📖 读取中` |
| `edit` | `editing` | `🛠️ Editing` | `🛠️ 编辑中` |
| `delete` | `deleting` | `🗑️ Deleting` | `🗑️ 删除中` |
| `move` | `moving` | `📦 Moving` | `📦 移动中` |
| `search` | `searching` | `🔎 Searching` | `🔎 搜索中` |
| `execute` | `running` | `🖥️ Running` | `🖥️ 执行中` |
| `think` | `thinking` | `🤔 Thinking` | `🤔 思考中` |
| `fetch` | `fetching` | `🌐 Fetching` | `🌐 获取中` |
| `switch_mode` | `switching` | `🔄 Switching mode` | `🔄 切换模式中` |
| `other` or unknown | `working` | `🛠️ Working` | `🛠️ 处理中` |

The projection first exactly matches standard ACP kinds, then uses legacy and third-party Bridge aliases. `other` remains the final fallback. Reactions and active card bodies use the same localized phase label. Distinguishing ACP `Agent` from `Other` requires new protocol metadata; the current protocol normalizes both to `other`.

The running card displays only the allowlisted phase label. ACP tool titles are not projected because built-in tools may derive them from commands, paths, or parameters. Reactions remain phase-only. The local bridge retains the safe tool kind from the initial event so kindless terminal updates can drive `Retrying` or return to `Thinking`; meta-only shell-progress heartbeats remain ignored and do not create another response boundary.

The running card's `statusLine` contains only the configured model and elapsed time. On completion, the process line is removed from the body so only the final assistant response remains; the existing terminal state, model, and elapsed time stay in `statusLine`. Tool descriptions, paths, commands, parameters, raw input, raw output, and model reasoning are never added to card content.

Phase and terminal labels use the effective Qwen display language after environment override, configured-language selection, and `auto` system-language detection. Presentation language never changes the agent prompt or tool-call schema.

When named-task attribution supplies a source label, the phase remains the first running-state line and the escaped source label stays above the response content throughout running, streaming, fallback, and terminal card states.

## Delivery modes

Lifecycle presentation is driven by channel lifecycle events, independently of response delivery. Plain replies, interactive status cards, and block-streaming cards therefore share the same inbound-message tag behavior. Interactive cards also project the current phase into the body; block streaming does not create a status card and continues to rely on the inbound-message tags for progress.

Reaction failures and status-card metadata failures are isolated from each other and from response delivery.

## Cleanup

Prompt cleanup, session death, and adapter disconnect recall both transient tags without adding a terminal result when the real outcome is unknown.
280 changes: 280 additions & 0 deletions docs/plans/2026-08-31-dingtalk-lifecycle-delivery-convergence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
# DingTalk Lifecycle Delivery Convergence Implementation Plan

> Historical plan: later product review approved a bounded tool-title summary in the active card and a minimal `AcpBridge` partial-update fix. The final contract is `docs/design/dingtalk-dynamic-lifecycle-tags.md`; the phase-only steps below preserve the original implementation sequence rather than the final scope.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R1-12: Both committed plan documents' status notes claim product review approved displaying restricted tool titles in the active card's phase line, while the final contract they cite in the same sentence — docs/design/dingtalk-dynamic-lifecycle-tags.md — states the opposite ('The running card displays only the allowlisted phase label. ACP tool titles are not projected because built-in tools may derive them from commands, paths, or parameters'), and the shipped code agrees with the design doc (grep-verified: no title reaches any card). The status notes are the only parts of these committed docs that present themselves as current — everything below is explicitly disclaimed as historical. A maintainer (or agentic worker, which these plans explicitly address) scoping a follow-up from them concludes the approved scope includes tool-title display; at worst they implement it, violating the privacy contract the design doc exists to enforce. The sibling plan file carries the same claim.

Witness:

not run — contradiction between committed texts settled by direct read at the reviewed commit plus a grep of the shipped presentation path (no title reaches any card); no runnable oracle exists.

Suggested fix: Reword the status note to match the final contract: the product outcome kept the card phase line phase-only (ACP tool titles are not projected), while the scope did grow to include the minimal AcpBridge partial-update fix — which the code corroborates.

Note: The fix must not re-introduce title projection to reconcile the note — the contract forbids it: docs/design/dingtalk-dynamic-lifecycle-tags.md — 'The running card displays only the allowlisted phase label. ACP tool titles are not projected'.

中文说明

[Suggestion] 两份已提交的计划文档的状态说明都声称产品评审批准了在活动卡片阶段行展示受限的工具 title,而同一句话中引用的最终契约——docs/design/dingtalk-dynamic-lifecycle-tags.md——表述相反('运行中的卡片只显示白名单内的阶段标签。不投影 ACP 工具 title,因为内置工具的 title 可能来自命令、路径或参数'),且已交付的代码与设计文档一致(已用 grep 验证:没有任何 title 到达卡片)。状态说明是这两份已提交文档中唯一以当前状态自居的部分——其下内容均被明确声明为历史记录。维护者(或这些计划明确面向的执行 Agent)据此规划后续工作时,会得出批准范围包含工具 title 展示的结论;最坏情况是其实现 title 投影,违反设计文档赖以存在的隐私契约。另一份孪生计划文档也有相同表述。

修复建议:将状态说明改写为与最终契约一致:产品结果使卡片阶段行仅展示阶段标签(不投影 ACP 工具 title),同时范围确实扩展为包含最小的 AcpBridge 部分更新修复——代码佐证了这一点。

注意:修复不应为了自洽而重新引入 title 投影——契约禁止它:docs/design/dingtalk-dynamic-lifecycle-tags.md——'运行中的卡片只显示白名单内的阶段标签。不投影 ACP 工具 title'。

— qwen3.8-max via Qwen Code /review (v0.22.3)


> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Deliver the approved phase-only DingTalk lifecycle prototype without exposing tool details, accumulating stale reactions, mis-resolving `auto` language, or dropping current-main source attribution.

**Architecture:** Keep lifecycle classification in the DingTalk adapter and project only a finite phase enum into reactions and the interactive card. Replace per-event reaction queueing with one desired-state drain per inbound message, where new phases overwrite pending phases and terminal states preempt them. Resolve the effective CLI language before constructing any channel, then merge current `origin/main` and preserve its source-label contract.

**Tech Stack:** TypeScript, Vitest, DingTalk emotion API, DingTalk interactive cards, Qwen CLI i18n.

**Spec:** `docs/design/dingtalk-dynamic-lifecycle-tags.md`

## Global Constraints

- The UI may display lifecycle phase labels and assistant response text only; it must not display tool title, description, path, command, parameters, output, or reasoning.
- `👀` remains stable while a single replaceable phase reaction is active.
- Phase updates are latest-wins; `completed`, `failed`, and `cancelled` preempt pending phases.
- A failed recall prevents a contradictory replacement from being attached.
- Status cards are created on `started`, stream response text below the phase, and remove the phase at terminal completion.
- Preserve current-main named-task source labels above response content in running, streaming, fallback, and terminal cards while keeping the lifecycle phase first during active runs.
- Do not change the DingTalk card template or add dependencies.

---

### Task 1: Enforce the phase-only presentation boundary

**Files:**

- Modify: `packages/channels/base/src/ChannelBase.test.ts`
- Modify: `packages/channels/base/src/ChannelBase.ts`
- Modify: `packages/channels/base/src/types.ts`
- Modify: `packages/channels/dingtalk/src/presentation-phase.ts`
- Modify: `packages/channels/dingtalk/src/DingtalkAdapter.test.ts`
- Modify: `packages/channels/dingtalk/src/DingtalkAdapter.ts`
- Modify: `packages/channels/dingtalk/src/interaction-presenter.ts`
- Modify: `packages/channels/dingtalk/src/status-card-controller.test.ts`
- Modify: `packages/channels/dingtalk/src/status-card-controller.ts`

**Interfaces:**

- Consumes: `ChannelTaskLifecycleEvent` with sanitized `kind`, `title`, and `status`.
- Produces: `lifecyclePresentationPhase(event): DingtalkPresentationPhase | undefined`; no tool-detail presentation type or card-detail API.

- [ ] **Step 1: Change the ChannelBase regression test so raw input, including `rawInput.description`, is absent from emitted lifecycle events while the adapter-owned tool callback still receives the original event.**

```ts
expect(lifecycleToolCall!.toolCall).not.toHaveProperty('description');
expect(lifecycleToolCall!.toolCall).not.toHaveProperty('rawInput');
expect(ch.toolCalls[0]!.event.rawInput).toEqual({
command: 'echo $SECRET',
description: 'Check disk health\nwithout exposing commands',
});
```

- [ ] **Step 2: Run the focused ChannelBase test and verify RED because the current sanitizer emits `description`.**

Run: `cd packages/channels/base && npx vitest run src/ChannelBase.test.ts -t "raw tool input"`

Expected: FAIL showing the lifecycle event still contains `description`.

- [ ] **Step 3: Change DingTalk tests to send a tool event whose title and description contain sensitive literals, then assert the card update receives only the mapped phase and card content contains none of those literals.**

```ts
expect(updateStatusCardPhase).toHaveBeenCalledWith('run-1', 'running');
expect(updateStatusCardTool).not.toBeDefined();
expect(streamedContent).toBe('🖥️ 执行中');
expect(streamedContent).not.toContain('/private/project');
expect(streamedContent).not.toContain('grep SECRET');
```

- [ ] **Step 4: Run the focused DingTalk tests and verify RED because the current tool-detail path is called and rendered.**

Run: `cd packages/channels/dingtalk && npx vitest run src/DingtalkAdapter.test.ts src/status-card-controller.test.ts -t "phase|detail|card body"`

Expected: FAIL showing `updateStatusCardTool` or a Markdown detail bullet.

- [ ] **Step 5: Remove `SanitizedToolCallEvent.description`, the `rawInput.description` extraction, `DingtalkToolPresentation`, `lifecycleToolPresentation`, `updateStatusCardTool`, status-card detail collections, and the DingTalk prompt instruction that asks the model to produce descriptions. Route every tool event through `lifecyclePresentationPhase` only.**

```ts
const presentationPhase = lifecyclePresentationPhase(event);
if (event.runId && presentationPhase) {
this.interactionPresenter?.updateStatusCardPhase(
event.runId,
presentationPhase,
);
}
```

- [ ] **Step 6: Run the three focused test files and verify GREEN.**

Run: `cd packages/channels/base && npx vitest run src/ChannelBase.test.ts && cd ../../dingtalk && npx vitest run src/DingtalkAdapter.test.ts src/status-card-controller.test.ts`

Expected: all tests pass with no sensitive literal in any card payload assertion.

### Task 2: Coalesce reaction transitions and preempt with terminal state

**Files:**

- Modify: `packages/channels/dingtalk/src/DingtalkAdapter.test.ts`
- Modify: `packages/channels/dingtalk/src/DingtalkAdapter.ts`

**Interfaces:**

- Consumes: phase and terminal lifecycle events for one stable inbound message.
- Produces: one reaction drain whose mutable desired phase is overwritten by newer phases and whose terminal request has priority.

- [ ] **Step 1: Add a failing latest-wins test that blocks a phase recall, emits `searching`, `running`, and `replying`, releases the recall, and expects only `replying` to be attached.**

```ts
expect(attachReaction.mock.calls.map(([, , tag]) => tag.name)).toEqual([
'👀',
'🤔 Thinking',
'✍️ Replying',
]);
```

- [ ] **Step 2: Run that test and verify RED because the existing `tail` enqueues and attaches every intermediate phase.**

Run: `cd packages/channels/dingtalk && npx vitest run src/DingtalkAdapter.test.ts -t "latest phase"`

Expected: FAIL with intermediate `Searching` and `Running` attachments.

- [ ] **Step 3: Add a failing terminal-preemption test that blocks the initial eye attach, emits multiple phases and `cancelled`, releases the attach, and expects no transient status attachment before `⏹️ Stopped`.**

```ts
expect(attachReaction.mock.calls.map(([, , tag]) => tag.name)).toEqual([
'👀',
'⏹️ Stopped',
]);
```

- [ ] **Step 4: Run that test and verify RED because the queued start and phase operations currently run before terminal cleanup.**

Run: `cd packages/channels/dingtalk && npx vitest run src/DingtalkAdapter.test.ts -t "terminal preempts"`

Expected: FAIL with obsolete transient attachments.

- [ ] **Step 5: Replace per-phase `enqueueReaction` calls with mutable state (`desiredStatusTag`, `terminalTag`, `drainScheduled`) and a single `drainReactionState` loop. Re-read desired state after each awaited recall and before each attach; if terminal is set, clear the current status and eye, attach exactly the terminal tag, then forget the state.**

```ts
state.desiredStatusTag = tag;
this.scheduleReactionDrain(state);

while (this.reactionStates.get(state.key) === state) {
if (state.terminalTag) {
await this.finishReactionState(state);
return;
}
const desired = state.desiredStatusTag;
if (!desired || desired.name === state.statusTag?.name) return;
if (state.statusTag && !(await this.recallReaction(...))) return;
if (state.terminalTag) continue;
const latest = state.desiredStatusTag;
if (latest && (await this.attachReaction(..., latest)) !== false) {
state.statusTag = latest;
}
}
```

- [ ] **Step 6: Run all DingTalk adapter tests and verify GREEN, including recall-failure and disconnect/session-death cleanup cases.**

Run: `cd packages/channels/dingtalk && npx vitest run src/DingtalkAdapter.test.ts`

Expected: all adapter tests pass and no test observes an obsolete terminal-delayed phase.

### Task 3: Resolve the effective display language at channel construction

**Files:**

- Modify: `packages/cli/src/i18n/index.ts`
- Modify: `packages/cli/src/commands/channel/daemon-worker.test.ts`
- Modify: `packages/cli/src/commands/channel/daemon-worker.ts`
- Modify: `packages/cli/src/commands/channel/start.test.ts`
- Modify: `packages/cli/src/commands/channel/start.ts`

**Interfaces:**

- Consumes: `QWEN_CODE_LANG`, `general.language`, and system locale.
- Produces: resolved `SupportedLanguage` in `ChannelBaseOptions.displayLanguage`; never the literal `auto`.

- [ ] **Step 1: Add failing command tests for `general.language: 'auto'`, mocking system detection to `zh`, and assert `displayLanguage: 'zh'`; add an env-precedence case where `QWEN_CODE_LANG=zh` overrides an English setting.**

```ts
expect(createChannel).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.anything(),
expect.objectContaining({ displayLanguage: 'zh' }),
);
```

- [ ] **Step 2: Run the two command test files and verify RED because the current code forwards `auto` or the raw setting.**

Run: `cd packages/cli && npx vitest run src/commands/channel/start.test.ts src/commands/channel/daemon-worker.test.ts`

Expected: FAIL with `displayLanguage: 'auto'` or the lower-priority setting.

- [ ] **Step 3: Export the existing `resolveLanguage` helper and pass `resolveLanguage(resolveLanguageSetting(configuredLanguage))` from both direct-start and daemon-worker entry points.**

```ts
const displayLanguage = resolveLanguage(
resolveLanguageSetting(settings.merged.general?.language as string),
);
```

- [ ] **Step 4: Run the i18n and command tests and verify GREEN.**

Run: `cd packages/cli && npx vitest run src/i18n/index.test.ts src/commands/channel/start.test.ts src/commands/channel/daemon-worker.test.ts`

Expected: all tests pass with explicit, auto-detected, and environment-overridden language cases.

### Task 4: Integrate current main without regressing source labels

**Files:**

- Modify on merge conflict: `packages/channels/dingtalk/src/interaction-presenter.ts`
- Modify on merge conflict: `packages/channels/dingtalk/src/interaction-presenter.test.ts`
- Modify on merge conflict if required: `packages/channels/dingtalk/src/DingtalkAdapter.ts`
- Modify: `docs/design/dingtalk-dynamic-lifecycle-tags.md`

**Interfaces:**

- Consumes: current-main `registerRun(..., sourceLabel?)` and `getResponseSourceLabel(sessionId)`.
- Produces: source-prefixed lifecycle card content plus the approved localized phase labels.

- [ ] **Step 1: Commit the green convergence changes so the dirty worktree is recoverable, then merge `origin/main` without rewriting published history.**

Run: `git merge --no-edit origin/main`

Expected: the known interaction-presenter conflict is surfaced for explicit resolution.

- [ ] **Step 2: Resolve the conflict by retaining the sixth `sourceLabel` argument and `withSourcePrefix` behavior from current main while retaining `updateStatusCardPhase` and phase-first active content.**

```ts
this.interactionPresenter?.registerRun(
event.runId,
event.owner.id,
inboundOwner.target,
event.sessionId,
inboundOwner.sender,
this.getResponseSourceLabel(event.sessionId),
);
```

- [ ] **Step 3: Update the design to state phase-only safety, desired-state coalescing, terminal preemption, effective language resolution, and source-label composition.**

- [ ] **Step 4: Run the focused DingTalk presenter/controller/adapter tests and verify GREEN after the merge.**

Run: `cd packages/channels/dingtalk && npx vitest run src/interaction-presenter.test.ts src/status-card-controller.test.ts src/DingtalkAdapter.test.ts`

Expected: all tests pass, including source labels through running, streaming, and terminal cards.

### Task 5: Verify and deliver the existing Draft PR

**Files:**

- Update remote PR: `https://github.com/QwenLM/qwen-code/pull/10504`

**Interfaces:**

- Consumes: the final merged worktree and repository PR template.
- Produces: pushed branch, current PR body, CI/readback evidence, and an explicit live-E2E evidence boundary.

- [ ] **Step 1: Format changed files, run `git diff --check`, and run focused tests, build, typecheck, and lint from the final tree.**

Run: `npx prettier --check <changed-files> && git diff --check && npm run build && npm run typecheck && npm run lint`

Expected: every command exits 0.

- [ ] **Step 2: Perform the repository self-audit over the complete `origin/main...HEAD` diff until two consecutive passes find no issue; any fix resets the clean-pass count and reruns verification.**

- [ ] **Step 3: Request an independent code review against exact final SHAs and fix every Critical or Important finding before proceeding.**

- [ ] **Step 4: Push the branch normally, update the Draft PR body from `.github/pull_request_template.md`, and read back the PR head, body, checks, and mergeability with `gh`.**

- [ ] **Step 5: Attempt live DingTalk E2E only if valid credentials are available without exposing them. If DingTalk returns credential error `40096`, record live delivery as unverified rather than treating local/loopback evidence as equivalent.**

- [ ] **Step 6: Report the exact delivered state, remaining external gates (review/CI/live credentials), and preserve the worktree for PR iteration.**
Loading
Loading