docs: consolidate design docs and plans under docs/ (#6417)

Design docs and implementation plans were scattered across .qwen/design,
.qwen/plans, and docs/superpowers. The .qwen/ locations are git-ignored, so
docs written there never got tracked, while docs/design already held the
richer, version-controlled set. Consolidate everything under docs/design and
docs/plans, relocate two stray root docs into docs/design, and repoint the
references left dangling by the move (moved-doc cross-links and a few source
comments).

Also update AGENTS.md and the feat-dev skill so the documented workflow writes
new design docs and plans to the tracked docs/ locations.

Co-authored-by: DragonnZhang <dragonzhang1024@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dragon 2026-07-07 14:05:05 +08:00 committed by GitHub
parent 4c884e47bd
commit 067cfbba62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 324 additions and 224 deletions

View file

@ -1,42 +0,0 @@
# Channel Lifecycle Status Umbrella
Date: 2026-07-01
## Goal
Provide one review surface that summarizes the lifecycle-status behavior across
the supported channel adapters and calls out what remains intentionally out of
scope.
## Scope
- Telegram
- Weixin
- DingTalk
- Feishu
## Explicit Non-Goals
- Slack remains out of scope.
- QQ Bot remains out of scope for lifecycle status UI.
- The plugin example remains out of scope for lifecycle status UI.
- DingTalk terminal emoji remains out of scope.
## Reviewer Matrix
| Channel | Supported lifecycle events | Native surface | `started` behavior | `text_chunk` behavior | Terminal behavior | Unsupported / no-op reason | Exact test files |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Telegram | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Starts the existing per-chat typing loop once. Duplicate `started` events do not add another loop. | Ignored by the lifecycle hook. Response content continues through the normal reply path. | Stops the typing loop on any terminal event and leaves no stale interval behind. | `tool_call` has no native status surface and does not need adapter UI. | `packages/channels/telegram/src/TelegramAdapter.test.ts` |
| Weixin | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Calls `setTyping(chatId, true)` once for the active chat. Duplicate `started` events do not restack typing state. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Calls `setTyping(chatId, false)` on terminal events. Failed start attempts clear local state so a later `started` can retry. | `tool_call` has no separate status surface and no extra message should be sent. | `packages/channels/weixin/src/WeixinAdapter.test.ts` |
| DingTalk | `started`, `completed`, `cancelled`, `failed` | Eye reaction on the inbound message | Attaches the existing eye reaction once when a conversation id is available. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Recalls the eye reaction on terminal events, including late-resolving attach races after cancellation. | Direct robot webhook chats do not expose the conversation id needed for reactions, so lifecycle status is a no-op there. `tool_call` also has no UI in scope. | `packages/channels/dingtalk/src/DingtalkAdapter.test.ts` |
| Feishu | `started`, `completed`, `cancelled`, `failed` | Streaming card status label | Keeps the card in its running state and reserves space for the running label while the existing card stream is active. | Not consumed directly by the lifecycle hook. Content streaming remains owned by the existing response/card stream hook. | Finalizes the card status label as completed, cancelled, or failed without overwriting the streamed answer body. | `tool_call` stays hidden because the card already uses the answer stream plus terminal status labels only. | `packages/channels/feishu/src/adapter.test.ts`, `packages/channels/feishu/src/markdown.test.ts` |
| QQ Bot | None | None | No-op. | No-op. QQ Bot still streams reply chunks through outbound message sends, but not through lifecycle status updates. | No-op. | The channel has no typing or task-status endpoint, and `QQChannel` leaves `onPromptStart`, `onPromptEnd`, and `onTaskLifecycle` empty by design. | `packages/channels/qqbot/src/send.test.ts`, `packages/channels/qqbot/src/api.test.ts` |
| Plugin example | None | WebSocket protocol messages only | No-op for lifecycle status. | Streams response chunks over the mock protocol's `chunk` message type from `onResponseChunk`, outside lifecycle status handling. | Sends the final outbound message on response completion, outside lifecycle status handling. | The mock channel demonstrates transport wiring only; it has no native typing, reaction, or status surface. | `integration-tests/channel-plugin.test.ts` |
## Review Notes
- Feishu lifecycle `text_chunk` remains a no-op in the lifecycle hook. It does
not append or update answer content there.
- Slack is intentionally excluded from this matrix because it is out of scope.
- DingTalk terminal events only recall the existing eye reaction in this scope.
No terminal emoji is added.

View file

@ -14,9 +14,9 @@ feeds the next.
## Artifact Paths
Use `.qwen/` paths for planning artifacts:
Use these paths for planning artifacts:
- `.qwen/design/<feature>.md`
- `docs/design/<feature>.md`
- `.qwen/e2e-tests/<feature>.md`
## Phase 1: Investigate

View file

@ -160,7 +160,7 @@ npm run preflight # Full check: clean → install → format → lint → build
### General workflow
1. **Design doc for non-trivial work** — write one in `.qwen/design/` if the
1. **Design doc for non-trivial work** — write one in `docs/design/` if the
change touches multiple files or involves design decisions. Skip for small
bugfixes.
2. **Test plan for behavioral changes** — write an E2E test plan in
@ -220,11 +220,18 @@ applicable.
## Project Directories
Project artifacts live under `.qwen/`:
Design docs and implementation plans are committed under `docs/` so they are
tracked in version control:
| Directory | Purpose |
| -------------- | -------------------------------- |
| `docs/design/` | Design docs for planned features |
| `docs/plans/` | Implementation plans |
Other working artifacts live under `.qwen/` (git-ignored):
| Directory | Purpose |
| ----------------------- | ------------------------------------ |
| `.qwen/design/` | Design docs for planned features |
| `.qwen/e2e-tests/` | E2E test plans and results |
| `.qwen/issues/` | Issue drafts before filing on GitHub |
| `.qwen/pr-drafts/` | PR drafts before submitting |

View file

@ -73,12 +73,12 @@ reasoning?: false | { effort?: 'low' | 'medium' | 'high' | 'max'; budget_tokens?
Existing per-provider translators:
| Provider | File | Behavior |
| --- | --- | --- |
| DeepSeek | `provider/deepseek.ts:176-218` | nested → flat `reasoning_effort`; `low/medium→high`, `xhigh→max` |
| Anthropic | `anthropicContentGenerator.ts:521-593`, clamp `665-693`, beta hdr `393-431` | `output_config.effort` + thinking; `max``high` clamp + one-time warn; `effort-2025-11-24` beta |
| Gemini | `geminiContentGenerator.ts:107-146` | `thinkingConfig`/`thinkingLevel`; `low→LOW`, `high/max→HIGH` |
| OpenAI/GLM/DashScope | `openaiContentGenerator/pipeline.ts:689-717` (`buildReasoningConfig`), strip `597-602` | forwards/strips `reasoning_effort`; DashScope adds `preserve_thinking` |
| Provider | File | Behavior |
| -------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| DeepSeek | `provider/deepseek.ts:176-218` | nested → flat `reasoning_effort`; `low/medium→high`, `xhigh→max` |
| Anthropic | `anthropicContentGenerator.ts:521-593`, clamp `665-693`, beta hdr `393-431` | `output_config.effort` + thinking; `max``high` clamp + one-time warn; `effort-2025-11-24` beta |
| Gemini | `geminiContentGenerator.ts:107-146` | `thinkingConfig`/`thinkingLevel`; `low→LOW`, `high/max→HIGH` |
| OpenAI/GLM/DashScope | `openaiContentGenerator/pipeline.ts:689-717` (`buildReasoningConfig`), strip `597-602` | forwards/strips `reasoning_effort`; DashScope adds `preserve_thinking` |
Gaps: the union lacks `xhigh`; Gemini lacks `medium` and an `xhigh→high` rule;
the generic pipeline must be confirmed to emit `reasoning_effort` for plain
@ -119,7 +119,7 @@ borrow from (studied at `~/Documents/openclaw`):
What we take: the **rank-based central clamp**, **per-model capability
declaration**, the **three shape mappers**, and the **exact Gemini 2.5 budget
buckets**. What we drop for v1: `minimal`/`adaptive` user tiers (decision = 5
tiers) — they stay valid *internal* normalization targets so a model catalog can
tiers) — they stay valid _internal_ normalization targets so a model catalog can
still declare them.
## Design
@ -132,13 +132,13 @@ Each provider declares a supported subset; the translator clamps a requested
tier **down** the ladder to the nearest supported tier. Mapping (canonical →
wire value), with `↓` marking a clamp:
| Tier | OpenAI `reasoning_effort` | DeepSeek `reasoning_effort` | GLM-5.2+ `reasoning_effort` | Anthropic `output_config.effort` | Gemini 3 `thinking_level` | Qwen DashScope |
| --- | --- | --- | --- | --- | --- | --- |
| low | low | high¹ | low | low | low | enable_thinking:true |
| medium | medium | high¹ | medium | medium | medium | true |
| high | high | high | high | high (default) | high | true |
| xhigh | xhigh | max¹ | xhigh | xhigh ↓high² | high ↓² | true |
| max | xhigh ↓ (no `max`) | max | max | max ↓high² | high ↓² | true |
| Tier | OpenAI `reasoning_effort` | DeepSeek `reasoning_effort` | GLM-5.2+ `reasoning_effort` | Anthropic `output_config.effort` | Gemini 3 `thinking_level` | Qwen DashScope |
| ------ | ------------------------- | --------------------------- | --------------------------- | -------------------------------- | ------------------------- | -------------------- |
| low | low | high¹ | low | low | low | enable_thinking:true |
| medium | medium | high¹ | medium | medium | medium | true |
| high | high | high | high | high (default) | high | true |
| xhigh | xhigh | max¹ | xhigh | xhigh ↓high² | high ↓² | true |
| max | xhigh ↓ (no `max`) | max | max | max ↓high² | high ↓² | true |
¹ DeepSeek/GLM documented internal grouping (low/medium ≡ high, xhigh ≡ max).
² Clamped to the model's documented ceiling (varies by Anthropic model; Gemini 3
@ -183,12 +183,12 @@ nested `reasoning: { effort }` object; `buildReasoningConfig()`
provider whose wire field differs must reshape it in its `buildRequest` hook.
Known shapes:
| Wire shape | Providers | qwen-code handling |
| --- | --- | --- |
| nested `reasoning: { effort }` | OpenAI Responses, OpenRouter, gpt-5.x | passthrough (default) ✅ |
| flat top-level `reasoning_effort` | DeepSeek, **GLM/z.ai**, OpenAI Chat Completions, Groq | DeepSeek adapter flattens ✅; **GLM has no adapter → currently ships the nested shape, likely wrong ❌** |
| `enable_thinking` bool | qwen3 / DashScope | adapter emits bool (disable only); no effort tiers yet |
| `extra_body.thinking.enabled` toggle | GLM | separate on/off knob from the effort value |
| Wire shape | Providers | qwen-code handling |
| ------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| nested `reasoning: { effort }` | OpenAI Responses, OpenRouter, gpt-5.x | passthrough (default) ✅ |
| flat top-level `reasoning_effort` | DeepSeek, **GLM/z.ai**, OpenAI Chat Completions, Groq | DeepSeek adapter flattens ✅; **GLM has no adapter → currently ships the nested shape, likely wrong ❌** |
| `enable_thinking` bool | qwen3 / DashScope | adapter emits bool (disable only); no effort tiers yet |
| `extra_body.thinking.enabled` toggle | GLM | separate on/off knob from the effort value |
Implication: pure passthrough only "just works" for providers that accept the
nested shape. **PR1 must add GLM/z.ai flattening** (mirror `deepseek.ts`) and,

View file

@ -34,26 +34,26 @@ scope because each channel already has a clear native surface for these states.
## Current State
| Channel | Existing status surface | Current behavior |
| --- | --- | --- |
| Telegram | Typing indicator | Starts typing on prompt start and stops on prompt end. |
| Weixin | Typing indicator | Starts typing on prompt start and stops on prompt end. |
| DingTalk | Message reaction | Adds the eye reaction on prompt start and recalls it on prompt end. |
| Feishu | Streaming card | Shows and updates a streaming card, with completion and error paths. |
| Channel | Existing status surface | Current behavior |
| -------- | ----------------------- | -------------------------------------------------------------------- |
| Telegram | Typing indicator | Starts typing on prompt start and stops on prompt end. |
| Weixin | Typing indicator | Starts typing on prompt start and stops on prompt end. |
| DingTalk | Message reaction | Adds the eye reaction on prompt start and recalls it on prompt end. |
| Feishu | Streaming card | Shows and updates a streaming card, with completion and error paths. |
## Proposed Design
Keep the implementation adapter-local. Each adapter consumes the lifecycle event
hook and maps the event into the platform's existing native status surface.
| Lifecycle event | Telegram | Weixin | DingTalk | Feishu |
| --- | --- | --- | --- | --- |
| `started` | Start typing. | Start typing. | Add eye reaction. | Show/update card as running. |
| `text_chunk` | Ignore. | Ignore. | Ignore. | Ignore in the lifecycle hook. Content streaming stays on the existing response/card stream path. |
| `tool_call` | Ignore. | Ignore. | Ignore. | Ignore for UI. |
| `completed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card completed. |
| `cancelled` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card cancelled. |
| `failed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card failed. |
| Lifecycle event | Telegram | Weixin | DingTalk | Feishu |
| --------------- | ------------- | ------------- | -------------------- | ------------------------------------------------------------------------------------------------ |
| `started` | Start typing. | Start typing. | Add eye reaction. | Show/update card as running. |
| `text_chunk` | Ignore. | Ignore. | Ignore. | Ignore in the lifecycle hook. Content streaming stays on the existing response/card stream path. |
| `tool_call` | Ignore. | Ignore. | Ignore. | Ignore for UI. |
| `completed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card completed. |
| `cancelled` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card cancelled. |
| `failed` | Stop typing. | Stop typing. | Recall eye reaction. | Mark card failed. |
### Telegram
@ -85,12 +85,12 @@ send extra status messages unless an existing error path already does so.
Feishu keeps the streaming card as the status surface and makes the terminal
state explicit in card content:
| State | Card label |
| --- | --- |
| Running | `运行中...` |
| Completed | `已完成` |
| Cancelled | `已取消` |
| Failed | `已失败,请重试` |
| State | Card label |
| --------- | ---------------- |
| Running | `运行中...` |
| Completed | `已完成` |
| Cancelled | `已取消` |
| Failed | `已失败,请重试` |
The card still streams answer content as it does today through the existing
response/card stream hook. Lifecycle `text_chunk` is not consumed directly by

View file

@ -0,0 +1,42 @@
# Channel Lifecycle Status Umbrella
Date: 2026-07-01
## Goal
Provide one review surface that summarizes the lifecycle-status behavior across
the supported channel adapters and calls out what remains intentionally out of
scope.
## Scope
- Telegram
- Weixin
- DingTalk
- Feishu
## Explicit Non-Goals
- Slack remains out of scope.
- QQ Bot remains out of scope for lifecycle status UI.
- The plugin example remains out of scope for lifecycle status UI.
- DingTalk terminal emoji remains out of scope.
## Reviewer Matrix
| Channel | Supported lifecycle events | Native surface | `started` behavior | `text_chunk` behavior | Terminal behavior | Unsupported / no-op reason | Exact test files |
| -------------- | --------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Telegram | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Starts the existing per-chat typing loop once. Duplicate `started` events do not add another loop. | Ignored by the lifecycle hook. Response content continues through the normal reply path. | Stops the typing loop on any terminal event and leaves no stale interval behind. | `tool_call` has no native status surface and does not need adapter UI. | `packages/channels/telegram/src/TelegramAdapter.test.ts` |
| Weixin | `started`, `completed`, `cancelled`, `failed` | Typing indicator | Calls `setTyping(chatId, true)` once for the active chat. Duplicate `started` events do not restack typing state. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Calls `setTyping(chatId, false)` on terminal events. Failed start attempts clear local state so a later `started` can retry. | `tool_call` has no separate status surface and no extra message should be sent. | `packages/channels/weixin/src/WeixinAdapter.test.ts` |
| DingTalk | `started`, `completed`, `cancelled`, `failed` | Eye reaction on the inbound message | Attaches the existing eye reaction once when a conversation id is available. | Ignored by the lifecycle hook. Response content continues through the normal send path. | Recalls the eye reaction on terminal events, including late-resolving attach races after cancellation. | Direct robot webhook chats do not expose the conversation id needed for reactions, so lifecycle status is a no-op there. `tool_call` also has no UI in scope. | `packages/channels/dingtalk/src/DingtalkAdapter.test.ts` |
| Feishu | `started`, `completed`, `cancelled`, `failed` | Streaming card status label | Keeps the card in its running state and reserves space for the running label while the existing card stream is active. | Not consumed directly by the lifecycle hook. Content streaming remains owned by the existing response/card stream hook. | Finalizes the card status label as completed, cancelled, or failed without overwriting the streamed answer body. | `tool_call` stays hidden because the card already uses the answer stream plus terminal status labels only. | `packages/channels/feishu/src/adapter.test.ts`, `packages/channels/feishu/src/markdown.test.ts` |
| QQ Bot | None | None | No-op. | No-op. QQ Bot still streams reply chunks through outbound message sends, but not through lifecycle status updates. | No-op. | The channel has no typing or task-status endpoint, and `QQChannel` leaves `onPromptStart`, `onPromptEnd`, and `onTaskLifecycle` empty by design. | `packages/channels/qqbot/src/send.test.ts`, `packages/channels/qqbot/src/api.test.ts` |
| Plugin example | None | WebSocket protocol messages only | No-op for lifecycle status. | Streams response chunks over the mock protocol's `chunk` message type from `onResponseChunk`, outside lifecycle status handling. | Sends the final outbound message on response completion, outside lifecycle status handling. | The mock channel demonstrates transport wiring only; it has no native typing, reaction, or status surface. | `integration-tests/channel-plugin.test.ts` |
## Review Notes
- Feishu lifecycle `text_chunk` remains a no-op in the lifecycle hook. It does
not append or update answer content there.
- Slack is intentionally excluded from this matrix because it is out of scope.
- DingTalk terminal events only recall the existing eye reaction in this scope.
No terminal emoji is added.

View file

@ -14,7 +14,7 @@ PR [#4842][p4842] shipped the fields with an end-to-end runtime path at the
time. PR [#4870][p4870] then replaced the YAML parser to support block
scalars. This follow-up PR builds on both: it replaces the YAML
**stringifier** (PR #4870 left it hand-rolled — see
`docs/yaml-parser-replacement.md`), surfaces `mcpServers` + `hooks` on
`docs/design/yaml-parser-replacement.md`), surfaces `mcpServers` + `hooks` on
`SubagentConfig`, and wires them to the runtime so per-agent MCP servers
and hooks actually fire when a subagent runs.

View file

@ -58,12 +58,12 @@ use fewer visible rows:
The automated spacing assertions and terminal evidence use 100-column fixtures
for the changed rules:
| Scenario | Width | Baseline rows | PR1 rows | Delta | Evidence |
| --- | ---: | ---: | ---: | ---: | --- |
| Simple assistant reply | 100 | 2 | 1 | -1 | leading history spacer removed |
| Tool header with one-line result | 100 | 3 | 2 | -1 | header and result are adjacent |
| Three-tool expanded group with rendered results | 100 | 16 | 11 | -5 | one header/result spacer removed per tool result and one inter-tool separator removed between adjacent tools |
| Full representative fixture | 100 | 26 | 19 | -7 | same rendered content captured in tmux |
| Scenario | Width | Baseline rows | PR1 rows | Delta | Evidence |
| ----------------------------------------------- | ----: | ------------: | -------: | ----: | ------------------------------------------------------------------------------------------------------------ |
| Simple assistant reply | 100 | 2 | 1 | -1 | leading history spacer removed |
| Tool header with one-line result | 100 | 3 | 2 | -1 | header and result are adjacent |
| Three-tool expanded group with rendered results | 100 | 16 | 11 | -5 | one header/result spacer removed per tool result and one inter-tool separator removed between adjacent tools |
| Full representative fixture | 100 | 26 | 19 | -7 | same rendered content captured in tmux |
The snapshot diffs also cover the existing 80-column fixtures to confirm the
same row-count deltas in the current component test harness.

View file

@ -25,18 +25,19 @@ PR1 通过去除工具组内部多余空行,初步收紧了 TUI 垂直间距
### 2. 收紧问答间距
| 位置 | 改动前 | 改动后 |
|------|--------|--------|
| 用户消息上方 | 1 行空白 | 0由色带提供视觉分隔降级时保留 marginTop=1 |
| 模型输出上方 | 1 行空白 | 1 行空白(保留,区分思考过程和最终输出) |
| 工具调用/状态消息上方 | 1 行空白 | 0 |
| 思考文本末尾 | 可能有多余换行 | trimEnd() 避免双空行 |
| 位置 | 改动前 | 改动后 |
| --------------------- | -------------- | ----------------------------------------------- |
| 用户消息上方 | 1 行空白 | 0由色带提供视觉分隔降级时保留 marginTop=1 |
| 模型输出上方 | 1 行空白 | 1 行空白(保留,区分思考过程和最终输出) |
| 工具调用/状态消息上方 | 1 行空白 | 0 |
| 思考文本末尾 | 可能有多余换行 | trimEnd() 避免双空行 |
同一轮对话内的"回复 → 工具调用 → 回复"序列不再有多余空行,信息更紧凑连贯。
## 效果对比
**改动前:**
```
1 行空白)
> 帮我读取 package.json
@ -54,6 +55,7 @@ PR1 通过去除工具组内部多余空行,初步收紧了 TUI 垂直间距
```
**改动后:**
```
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
> 帮我读取 package.json

View file

@ -5,7 +5,7 @@ Internal design document for replacing the hand-rolled 192-line YAML parser at
`mcpServers` and `hooks` fields from Claude Code's declarative-agent schema can
round-trip safely through subagent / skill / converter code paths.
Companion to [`docs/declarative-agents-port.md`](./declarative-agents-port.md).
Companion to [`docs/design/declarative-agents-port.md`](./declarative-agents-port.md).
Issue: [#4821](https://github.com/QwenLM/qwen-code/issues/4821). Prereq for
the follow-up to [PR #4842](https://github.com/QwenLM/qwen-code/pull/4842).
@ -46,7 +46,7 @@ export function parseYaml(input: string): unknown {
(we don't target Bun runtime).
- **Schema mode**: NOT explicitly set anywhere in CC. Relies on `yaml`
package's default behavior, plus zod validation at the consumer layer
(`DL7`, `gS8`, `TKO`/`_u` per `docs/declarative-agents-port.md`). **C**
(`DL7`, `gS8`, `TKO`/`_u` per `docs/design/declarative-agents-port.md`). **C**
### Why `yaml` rather than `js-yaml`
@ -87,13 +87,13 @@ Track that decision separately if it comes up.
`~/code/claude-code/src/utils/frontmatterParser.ts` is 370 lines. Key
findings:
| Step | Logic | Source |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Delimiter match | Regex `/^---\s*\n([\s\S]*?)\n---\s*\n?/` — opens at column 0, body is non-greedy, closing `---` must be on its own line | `frontmatterParser.ts:~123` (line numbers from old snapshot; treat as approximate) **C** |
| Pass 1 parse | Call `parseYaml(body)`. If success → return parsed object + content remainder. | same file, top of try block **C** |
| Pass 2 recovery | On `YAMLException`, walk lines, auto-quote values that look like dates/colons/specials, retry `parseYaml` once. | lines ~85121 in old snapshot **C** (`tab → 2 spaces` normalisation, ISO-date heuristic, colon-trap) |
| Failure fallthrough | Both passes failed → log via `logForDebugging`, return `{ data: {}, content: text }`. Agent loads with empty frontmatter. | end of function **C** |
| Telemetry | Wrapped further upstream — `tengu_frontmatter_shadow_unknown_key` / `_mismatch` events fire from `ug5.agent` (Ig5 schema) | `claude.strings:308120`, `309074`, `309076` (cross-cited in `docs/declarative-agents-port.md` Phase 1) |
| Step | Logic | Source |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Delimiter match | Regex `/^---\s*\n([\s\S]*?)\n---\s*\n?/` — opens at column 0, body is non-greedy, closing `---` must be on its own line | `frontmatterParser.ts:~123` (line numbers from old snapshot; treat as approximate) **C** |
| Pass 1 parse | Call `parseYaml(body)`. If success → return parsed object + content remainder. | same file, top of try block **C** |
| Pass 2 recovery | On `YAMLException`, walk lines, auto-quote values that look like dates/colons/specials, retry `parseYaml` once. | lines ~85121 in old snapshot **C** (`tab → 2 spaces` normalisation, ISO-date heuristic, colon-trap) |
| Failure fallthrough | Both passes failed → log via `logForDebugging`, return `{ data: {}, content: text }`. Agent loads with empty frontmatter. | end of function **C** |
| Telemetry | Wrapped further upstream — `tengu_frontmatter_shadow_unknown_key` / `_mismatch` events fire from `ug5.agent` (Ig5 schema) | `claude.strings:308120`, `309074`, `309076` (cross-cited in `docs/design/declarative-agents-port.md` Phase 1) |
**Implication for qwen-code**: we do NOT need to clone the 2-pass recovery.
qwen-code's `subagent-manager.ts` already enforces stricter "throw on malformed
@ -105,7 +105,7 @@ warn-and-drop posture.
## Phase 3 — Nested validation via zod (CC)
The relevant CC validators per `docs/declarative-agents-port.md` Phase 1 +
The relevant CC validators per `docs/design/declarative-agents-port.md` Phase 1 +
binary strings cross-check:
### `mcpServers` (CC symbol `gS8` / JSON-shadow `jL7`)
@ -134,7 +134,7 @@ DL7-style), and let the downstream merge into `Config.getMcpServers()` do the
shape coercion. `qwen-code` already has `MCPServerConfig` class with
`type` discrimination — we reuse that converter instead of duplicating the
zod schema. See Phase 4 of the runtime-wiring plan in
`docs/declarative-agents-port.md`.
`docs/design/declarative-agents-port.md`.
### `hooks` (CC symbol `TKO` / `_u`)
@ -477,7 +477,7 @@ from the existing test suites in `packages/core/src/subagents/`,
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Q1 | Does `yaml.parse` need an explicit logger to redirect `YAMLWarning` (e.g., `Unresolved tag`) to qwen-code's logger instead of `process.emitWarning`? | No — defer | If logs get noisy in CI, plumb `{ logLevel: 'silent' }` or a custom `onWarning` callback. Not load-bearing for v1. |
| Q2 | Should `parse()` continue to return `{}` for empty-string / null-document YAML, or throw? | No — preserve current behavior | Current hand-rolled returns `{}`; we keep that. Add a regression test pinning the choice. |
| Q3 | When `mcpServers` is malformed at the top level (e.g., `mcpServers: "string"`), should the whole agent fail to load, or load with that field dropped? | Yes — drives the warn-and-drop posture in Phase 3 of the implementation | **Resolution**: drop the field, emit a console warning (parity with CC `DL7` per Phase 3 of `docs/declarative-agents-port.md`). |
| Q3 | When `mcpServers` is malformed at the top level (e.g., `mcpServers: "string"`), should the whole agent fail to load, or load with that field dropped? | Yes — drives the warn-and-drop posture in Phase 3 of the implementation | **Resolution**: drop the field, emit a console warning (parity with CC `DL7` per Phase 3 of `docs/design/declarative-agents-port.md`). |
| Q4 | Same as Q3 but for `hooks`: drop the field, the event, or just the individual matcher? | Yes — drives the warn-and-drop posture | **Resolution**: drop the whole `hooks` field on top-level shape failure. Per-event / per-matcher granularity is deferred to a future PR if a real user surfaces a need. |
| Q5 | Does the `Bun.YAML.parse` shortcut from CC's helper apply to qwen-code? | No | qwen-code does not target Bun runtime. Skip. |
@ -485,4 +485,4 @@ from the existing test suites in `packages/core/src/subagents/`,
**Status**: research complete, ready to implement Phase 2 (replace
`yaml-parser.ts`) and Phase 3 (re-surface `mcpServers` + `hooks` on
`SubagentConfig`) per `docs/declarative-agents-port.md`.
`SubagentConfig`) per `docs/design/declarative-agents-port.md`.

View file

@ -13,6 +13,7 @@
### Task 1: Extend UsageSummaryRecord with latency and tool duration
**Files:**
- Modify: `packages/core/src/services/usageHistoryService.ts:16-44`
- Modify: `packages/core/src/services/usageHistoryService.ts:111-158` (metricsToUsageRecord)
- Test: `packages/core/src/services/usageHistoryService.test.ts` (create)
@ -32,7 +33,13 @@ function makeMetrics(): SessionMetrics {
models: {
'qwen-max': {
api: { totalRequests: 5, totalErrors: 0, totalLatencyMs: 9500 },
tokens: { prompt: 1000, candidates: 500, total: 1500, cached: 800, thoughts: 0 },
tokens: {
prompt: 1000,
candidates: 500,
total: 1500,
cached: 800,
thoughts: 0,
},
bySource: {},
},
},
@ -48,8 +55,30 @@ function makeMetrics(): SessionMetrics {
[ToolCallDecision.AUTO_ACCEPT]: 4,
},
byName: {
edit: { count: 6, success: 6, fail: 0, durationMs: 3000, decisions: { [ToolCallDecision.ACCEPT]: 3, [ToolCallDecision.REJECT]: 0, [ToolCallDecision.MODIFY]: 0, [ToolCallDecision.AUTO_ACCEPT]: 3 } },
bash: { count: 4, success: 3, fail: 1, durationMs: 2000, decisions: { [ToolCallDecision.ACCEPT]: 2, [ToolCallDecision.REJECT]: 1, [ToolCallDecision.MODIFY]: 0, [ToolCallDecision.AUTO_ACCEPT]: 1 } },
edit: {
count: 6,
success: 6,
fail: 0,
durationMs: 3000,
decisions: {
[ToolCallDecision.ACCEPT]: 3,
[ToolCallDecision.REJECT]: 0,
[ToolCallDecision.MODIFY]: 0,
[ToolCallDecision.AUTO_ACCEPT]: 3,
},
},
bash: {
count: 4,
success: 3,
fail: 1,
durationMs: 2000,
decisions: {
[ToolCallDecision.ACCEPT]: 2,
[ToolCallDecision.REJECT]: 1,
[ToolCallDecision.MODIFY]: 0,
[ToolCallDecision.AUTO_ACCEPT]: 1,
},
},
},
},
files: { totalLinesAdded: 50, totalLinesRemoved: 10 },
@ -58,12 +87,24 @@ function makeMetrics(): SessionMetrics {
describe('metricsToUsageRecord', () => {
it('includes totalLatencyMs from all models', () => {
const record = metricsToUsageRecord('s1', '/proj', 1000, 2000, makeMetrics());
const record = metricsToUsageRecord(
's1',
'/proj',
1000,
2000,
makeMetrics(),
);
expect(record.totalLatencyMs).toBe(9500);
});
it('includes per-tool totalDurationMs in byName', () => {
const record = metricsToUsageRecord('s1', '/proj', 1000, 2000, makeMetrics());
const record = metricsToUsageRecord(
's1',
'/proj',
1000,
2000,
makeMetrics(),
);
expect(record.tools.byName['edit']!.totalDurationMs).toBe(3000);
expect(record.tools.byName['bash']!.totalDurationMs).toBe(2000);
});
@ -103,7 +144,10 @@ export interface UsageSummaryRecord {
totalCalls: number;
totalSuccess: number;
totalFail: number;
byName: Record<string, { count: number; success: number; fail: number; totalDurationMs?: number }>;
byName: Record<
string,
{ count: number; success: number; fail: number; totalDurationMs?: number }
>;
};
files: {
linesAdded: number;
@ -186,6 +230,7 @@ git commit -m "feat(stats): extend UsageSummaryRecord with latency and tool dura
### Task 2: Add delta calculation and aggregation extensions
**Files:**
- Modify: `packages/core/src/services/usageHistoryService.ts:283-394` (aggregateUsage)
- Test: `packages/core/src/services/usageHistoryService.test.ts` (extend)
@ -194,9 +239,15 @@ git commit -m "feat(stats): extend UsageSummaryRecord with latency and tool dura
Add to `packages/core/src/services/usageHistoryService.test.ts`:
```typescript
import { aggregateUsage, type UsageSummaryRecord, type TimeRange } from './usageHistoryService.js';
import {
aggregateUsage,
type UsageSummaryRecord,
type TimeRange,
} from './usageHistoryService.js';
function makeRecord(overrides: Partial<UsageSummaryRecord> = {}): UsageSummaryRecord {
function makeRecord(
overrides: Partial<UsageSummaryRecord> = {},
): UsageSummaryRecord {
return {
version: 1,
sessionId: 's1',
@ -231,7 +282,10 @@ function makeRecord(overrides: Partial<UsageSummaryRecord> = {}): UsageSummaryRe
describe('aggregateUsage', () => {
it('includes totalLatencyMs in aggregated result', () => {
const records = [makeRecord({ totalLatencyMs: 2000 }), makeRecord({ totalLatencyMs: 3000 })];
const records = [
makeRecord({ totalLatencyMs: 2000 }),
makeRecord({ totalLatencyMs: 3000 }),
];
const report = aggregateUsage(records, 'all');
expect(report.totalLatencyMs).toBe(5000);
});
@ -452,6 +506,7 @@ git commit -m "feat(stats): add latency/duration/requests to aggregated report"
### Task 3: Add delta calculation to statsDataService
**Files:**
- Modify: `packages/cli/src/ui/utils/statsDataService.ts`
- Test: `packages/cli/src/ui/utils/statsDataService.test.ts` (create)
@ -465,7 +520,8 @@ import type { UsageSummaryRecord } from '@qwen-code/qwen-code-core';
// Mock loadUsageHistory to return controlled data
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const orig = await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
const orig =
await importOriginal<typeof import('@qwen-code/qwen-code-core')>();
return {
...orig,
loadUsageHistory: vi.fn(),
@ -500,7 +556,9 @@ function makeRecord(ts: number, tokens: number): UsageSummaryRecord {
totalCalls: 5,
totalSuccess: 4,
totalFail: 1,
byName: { edit: { count: 5, success: 4, fail: 1, totalDurationMs: 1000 } },
byName: {
edit: { count: 5, success: 4, fail: 1, totalDurationMs: 1000 },
},
},
files: { linesAdded: 10, linesRemoved: 5 },
};
@ -585,9 +643,12 @@ function computeDelta(
return ((cur - prev) / prev) * 100;
};
let curTokens = 0, prevTokens = 0;
let curInput = 0, prevInput = 0;
let curCached = 0, prevCached = 0;
let curTokens = 0,
prevTokens = 0;
let curInput = 0,
prevInput = 0;
let curCached = 0,
prevCached = 0;
for (const m of Object.values(current.models)) {
curTokens += m.totalTokens;
curInput += m.inputTokens;
@ -601,14 +662,22 @@ function computeDelta(
const curCacheRate = curInput > 0 ? (curCached / curInput) * 100 : 0;
const prevCacheRate = prevInput > 0 ? (prevCached / prevInput) * 100 : 0;
const curToolSuccess = current.tools.totalCalls > 0
? (current.tools.totalSuccess / current.tools.totalCalls) * 100 : 0;
const prevToolSuccess = previous.tools.totalCalls > 0
? (previous.tools.totalSuccess / previous.tools.totalCalls) * 100 : 0;
const curLatency = current.totalRequests > 0
? current.totalLatencyMs / current.totalRequests : null;
const prevLatency = previous.totalRequests > 0
? previous.totalLatencyMs / previous.totalRequests : null;
const curToolSuccess =
current.tools.totalCalls > 0
? (current.tools.totalSuccess / current.tools.totalCalls) * 100
: 0;
const prevToolSuccess =
previous.tools.totalCalls > 0
? (previous.tools.totalSuccess / previous.tools.totalCalls) * 100
: 0;
const curLatency =
current.totalRequests > 0
? current.totalLatencyMs / current.totalRequests
: null;
const prevLatency =
previous.totalRequests > 0
? previous.totalLatencyMs / previous.totalRequests
: null;
return {
sessions: pctChange(current.sessionCount, previous.sessionCount),
@ -616,8 +685,10 @@ function computeDelta(
tokens: pctChange(curTokens, prevTokens),
cacheRate: curCacheRate - prevCacheRate,
toolSuccess: curToolSuccess - prevToolSuccess,
avgLatency: curLatency !== null && prevLatency !== null
? curLatency - prevLatency : null,
avgLatency:
curLatency !== null && prevLatency !== null
? curLatency - prevLatency
: null,
};
}
```
@ -625,7 +696,9 @@ function computeDelta(
Add a helper to get previous range bounds:
```typescript
function getPreviousRangeBounds(range: TimeRange): { start: Date; end: Date } | null {
function getPreviousRangeBounds(
range: TimeRange,
): { start: Date; end: Date } | null {
if (range === 'all') return null;
const { start, end } = getTimeRangeBounds(range);
const durationMs = end.getTime() - start.getTime();
@ -653,24 +726,31 @@ export async function loadStatsData(
const prevBounds = getPreviousRangeBounds(range);
if (prevBounds) {
const prevFiltered = records.filter(
(r) => r.timestamp >= prevBounds.start.getTime() && r.timestamp < prevBounds.end.getTime(),
(r) =>
r.timestamp >= prevBounds.start.getTime() &&
r.timestamp < prevBounds.end.getTime(),
);
const prevReport = aggregateUsage(prevFiltered, 'all');
delta = computeDelta(report, prevReport);
}
// Efficiency
let totalInput = 0, totalCached = 0;
let totalInput = 0,
totalCached = 0;
for (const m of Object.values(report.models)) {
totalInput += m.inputTokens;
totalCached += m.cachedTokens;
}
const efficiency: StatsData['efficiency'] = {
cacheHitRate: totalInput > 0 ? (totalCached / totalInput) * 100 : 0,
toolSuccessRate: report.tools.totalCalls > 0
? (report.tools.totalSuccess / report.tools.totalCalls) * 100 : 0,
avgLatencyMs: report.totalRequests > 0
? report.totalLatencyMs / report.totalRequests : null,
toolSuccessRate:
report.tools.totalCalls > 0
? (report.tools.totalSuccess / report.tools.totalCalls) * 100
: 0,
avgLatencyMs:
report.totalRequests > 0
? report.totalLatencyMs / report.totalRequests
: null,
};
// Tool leaderboard
@ -765,6 +845,7 @@ git commit -m "feat(stats): add delta calculation, efficiency metrics, tool lead
### Task 4: Change heatmap to token-based with today highlight
**Files:**
- Modify: `packages/cli/src/ui/utils/statsDataService.ts:69-82` (buildHeatmap)
- Modify: `packages/cli/src/ui/utils/asciiCharts.ts` (HeatmapCell interface + buildHeatmapData)
@ -849,6 +930,7 @@ git commit -m "feat(stats): token-based heatmap with today highlight"
### Task 5: Add 'today' to TimeRange and update range cycle
**Files:**
- Modify: `packages/core/src/services/usageHistoryService.ts:46,253-281`
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx:34`
@ -888,6 +970,7 @@ git commit -m "feat(stats): add 'today' to range cycle"
### Task 6: Implement ActivityTab component
**Files:**
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx`
- [ ] **Step 1: Replace OverviewTab with ActivityTab**
@ -1085,6 +1168,7 @@ git commit -m "feat(stats): implement ActivityTab with KPI deltas, heatmap, tren
### Task 7: Implement EfficiencyTab component
**Files:**
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx`
- [ ] **Step 1: Replace ModelsTab with EfficiencyTab**
@ -1243,9 +1327,11 @@ Remove the `chartFilter` state and the `e` key handler (no longer needed).
Update the hints text:
```typescript
{activeTab === 'session'
? t('tab · esc')
: t('tab · r dates · ←→ month · esc')}
{
activeTab === 'session'
? t('tab · esc')
: t('tab · r dates · ←→ month · esc');
}
```
- [ ] **Step 3: Commit**
@ -1260,6 +1346,7 @@ git commit -m "feat(stats): implement EfficiencyTab with perf cards, tool leader
### Task 8: Add i18n keys
**Files:**
- Modify: `packages/cli/src/i18n/mustTranslateKeys.ts`
- [ ] **Step 1: Add new translation keys**
@ -1304,6 +1391,7 @@ git commit -m "feat(stats): add i18n keys for new dashboard tabs"
### Task 9: Clean up unused code and verify
**Files:**
- Modify: `packages/cli/src/ui/components/StatsDialog.tsx`
- [ ] **Step 1: Remove dead code**
@ -1323,6 +1411,7 @@ Expected: All pass (fix any snapshot updates with `--update` if needed).
- [ ] **Step 4: Visual verification**
Run: `npm run dev`, then type `/stats`:
- Verify Session tab unchanged
- Verify Activity tab shows KPI row with deltas, token heatmap with today highlight, sparkline, projects
- Verify Efficiency tab shows performance cards, tool leaderboard with bars, model table, code impact

View file

@ -8,7 +8,7 @@
**Tech Stack:** TypeScript, Vitest, Node `fs.promises`, existing `Storage.getGlobalDebugDir()`, existing `updateSymlink` helper.
**Reference spec:** `docs/superpowers/specs/2026-05-26-daemon-logger-design.md`
**Reference spec:** `docs/design/2026-05-26-daemon-logger-design.md`
**Test harness:** `vitest run` from each package; for a single file: `cd packages/<pkg> && npx vitest run <relative-path>`.
@ -47,7 +47,7 @@ Expected: all pass. (If not, baseline is broken — stop and report.)
- [ ] **Step 3: Skim the spec**
Read `docs/superpowers/specs/2026-05-26-daemon-logger-design.md` end-to-end. Key sections to internalize: §3 (modules), §4 (path), §5 (API), §6 (format + tee semantics), §7 (boot/shutdown), §11 (error handling).
Read `docs/design/2026-05-26-daemon-logger-design.md` end-to-end. Key sections to internalize: §3 (modules), §4 (path), §5 (API), §6 (format + tee semantics), §7 (boot/shutdown), §11 (error handling).
---

View file

@ -8,7 +8,7 @@
**Tech Stack:** TypeScript, Vitest, Express (REST routes), JSON-RPC (ACP), supertest (integration)
**Spec:** `docs/superpowers/specs/2026-05-27-daemon-workspace-service-design.md`
**Spec:** `docs/design/2026-05-27-daemon-workspace-service-design.md`
---

View file

@ -61,7 +61,7 @@ helpers must be idempotent and must not double-append streamed Feishu content.
**Files:**
- Read: `.qwen/design/2026-07-01-channel-lifecycle-status-adapters.md`
- Read: `docs/design/2026-07-01-channel-lifecycle-status-adapters.md`
- Read: `packages/channels/base/src/types.ts`
- Read: `packages/channels/base/src/ChannelBase.ts`
@ -308,8 +308,7 @@ Add the behavior test:
it('maps lifecycle start and terminal events to typing state', () => {
const channel = createChannel();
const setTyping = vi.fn().mockResolvedValue(undefined);
(channel as unknown as { setTyping: typeof setTyping }).setTyping =
setTyping;
(channel as unknown as { setTyping: typeof setTyping }).setTyping = setTyping;
const baseEvent = {
channelName: 'weixin',
@ -485,8 +484,9 @@ it('maps lifecycle start and terminal events to the eye reaction', () => {
it('does not attach lifecycle reactions without a conversation id', () => {
const channel = createChannel();
const attachReaction = vi.fn().mockResolvedValue(undefined);
(channel as unknown as { attachReaction: typeof attachReaction })
.attachReaction = attachReaction;
(
channel as unknown as { attachReaction: typeof attachReaction }
).attachReaction = attachReaction;
getLifecycleHook(channel)({
type: 'started',
@ -973,9 +973,7 @@ state:
const terminalStatus = cs.terminalStatus || 'failed';
const terminalLabel = this.statusLabelFor(terminalStatus);
const text = cs.accumulatedText
? (atPrefix
? `${atPrefix}\n\n${cs.accumulatedText}`
: cs.accumulatedText) +
? (atPrefix ? `${atPrefix}\n\n${cs.accumulatedText}` : cs.accumulatedText) +
'\n\n---\n' +
`*${terminalLabel}*`
: (atPrefix ? `${atPrefix}\n\n` : '') + `*${terminalLabel}*`;

View file

@ -20,6 +20,7 @@
## Task 1: Add Channel Metadata And Lifecycle Types
**Files:**
- Modify: `packages/channels/base/src/types.ts`
- Test: `packages/channels/base/src/ChannelBase.test.ts`
@ -199,8 +200,8 @@ Add private fields after `protected name: string;`:
Set them in the constructor after `this.proxy = options?.proxy;`:
```ts
this.identity = this.resolveIdentity(name, config);
this.memoryScope = this.resolveMemoryScope(name, config);
this.identity = this.resolveIdentity(name, config);
this.memoryScope = this.resolveMemoryScope(name, config);
```
Add methods near other protected hooks:
@ -247,15 +248,15 @@ Add methods near other protected hooks:
Emit `started` after `this.activePrompts.set(sessionId, promptState);`:
```ts
this.emitTaskLifecycle({
type: 'started',
channelName: this.name,
chatId: envelope.chatId,
sessionId,
messageId: envelope.messageId,
identity: this.identity,
memoryScope: this.memoryScope,
});
this.emitTaskLifecycle({
type: 'started',
channelName: this.name,
chatId: envelope.chatId,
sessionId,
messageId: envelope.messageId,
identity: this.identity,
memoryScope: this.memoryScope,
});
```
- [ ] **Step 5: Run test to verify Task 1 passes**
@ -272,6 +273,7 @@ Expected: both new metadata tests pass; existing tests still pass.
## Task 2: Add Prompt Boundary And Status Visibility
**Files:**
- Modify: `packages/channels/base/src/ChannelBase.ts`
- Test: `packages/channels/base/src/ChannelBase.test.ts`
@ -301,13 +303,13 @@ it('prepends channel boundary metadata before custom instructions once per sessi
expect(prompt).toContain('Channel identity:');
expect(prompt).toContain('- id: ops-agent');
expect(prompt).toContain('- display name: Ops Agent');
expect(prompt).toContain(
'- description: Coordinates repository operations.',
);
expect(prompt).toContain('- description: Coordinates repository operations.');
expect(prompt).toContain('Memory scope:');
expect(prompt).toContain('- namespace: qwen-tag:ops');
expect(prompt).toContain('- mode: metadata-only');
expect(prompt).toContain('- storage isolation: not enforced by this version.');
expect(prompt).toContain(
'- storage isolation: not enforced by this version.',
);
expect(prompt.indexOf('Channel identity:')).toBeLessThan(
prompt.indexOf('Be concise.'),
);
@ -378,25 +380,25 @@ Add private method near metadata resolvers:
Replace the existing instruction block:
```ts
if (this.config.instructions && !this.instructedSessions.has(sessionId)) {
promptText = `${this.config.instructions}\n\n${promptText}`;
this.instructedSessions.add(sessionId);
}
if (this.config.instructions && !this.instructedSessions.has(sessionId)) {
promptText = `${this.config.instructions}\n\n${promptText}`;
this.instructedSessions.add(sessionId);
}
```
with:
```ts
if (
this.shouldPrependChannelBoundaryPrompt() &&
!this.instructedSessions.has(sessionId)
) {
const prefix = this.config.instructions
? `${this.channelBoundaryPrompt()}\n\n${this.config.instructions}`
: this.channelBoundaryPrompt();
promptText = `${prefix}\n\n${promptText}`;
this.instructedSessions.add(sessionId);
}
if (
this.shouldPrependChannelBoundaryPrompt() &&
!this.instructedSessions.has(sessionId)
) {
const prefix = this.config.instructions
? `${this.channelBoundaryPrompt()}\n\n${this.config.instructions}`
: this.channelBoundaryPrompt();
promptText = `${prefix}\n\n${promptText}`;
this.instructedSessions.add(sessionId);
}
```
- [ ] **Step 4: Add status lines**
@ -429,6 +431,7 @@ Expected: prompt boundary and status tests pass; existing instruction tests are
## Task 3: Emit Full Task Lifecycle Events
**Files:**
- Modify: `packages/channels/base/src/ChannelBase.ts`
- Test: `packages/channels/base/src/ChannelBase.test.ts`
@ -538,25 +541,25 @@ Update `started` to spread `this.lifecycleBase(...)`.
In `bridgeToolCallListener`, after `this.onToolCall(target.chatId, event);`, add:
```ts
this.emitTaskLifecycle({
type: 'tool_call',
channelName: this.name,
chatId: target.chatId,
sessionId: event.sessionId,
toolCall: event,
identity: this.identity,
memoryScope: this.memoryScope,
});
this.emitTaskLifecycle({
type: 'tool_call',
channelName: this.name,
chatId: target.chatId,
sessionId: event.sessionId,
toolCall: event,
identity: this.identity,
memoryScope: this.memoryScope,
});
```
In `onChunk`, after `this.onResponseChunk(...)`, add:
```ts
this.emitTaskLifecycle({
type: 'text_chunk',
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
chunk,
});
this.emitTaskLifecycle({
type: 'text_chunk',
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
chunk,
});
```
- [ ] **Step 5: Emit cancellation events**
@ -564,31 +567,31 @@ In `onChunk`, after `this.onResponseChunk(...)`, add:
In `/cancel`, after `active.cancelled = true;`, add:
```ts
this.emitTaskLifecycle({
type: 'cancelled',
...this.lifecycleBase(active.chatId, activeSessionId, active.messageId),
reason: 'cancel_command',
});
this.emitTaskLifecycle({
type: 'cancelled',
...this.lifecycleBase(active.chatId, activeSessionId, active.messageId),
reason: 'cancel_command',
});
```
In `/clear`, when `active` exists before waiting, add:
```ts
this.emitTaskLifecycle({
type: 'cancelled',
...this.lifecycleBase(active.chatId, id, active.messageId),
reason: 'clear',
});
this.emitTaskLifecycle({
type: 'cancelled',
...this.lifecycleBase(active.chatId, id, active.messageId),
reason: 'clear',
});
```
In `steer`, after `active.cancelled = true;`, add:
```ts
this.emitTaskLifecycle({
type: 'cancelled',
...this.lifecycleBase(active.chatId, sessionId, active.messageId),
reason: 'steer',
});
this.emitTaskLifecycle({
type: 'cancelled',
...this.lifecycleBase(active.chatId, sessionId, active.messageId),
reason: 'steer',
});
```
- [ ] **Step 6: Emit completed and failed events**
@ -596,10 +599,10 @@ In `steer`, after `active.cancelled = true;`, add:
In the prompt `try` block, after response delivery finishes and only when `!promptState.cancelled`, add:
```ts
this.emitTaskLifecycle({
type: 'completed',
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
});
this.emitTaskLifecycle({
type: 'completed',
...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
});
```
Convert the `try/finally` into `try/catch/finally`:
@ -629,6 +632,7 @@ Expected: all `ChannelBase` tests pass.
## Task 4: Config Parsing, Exports, And Verification
**Files:**
- Modify: `packages/cli/src/commands/channel/config-utils.ts`
- Modify: `packages/cli/src/commands/channel/config-utils.test.ts`
- Modify: `packages/channels/base/src/index.ts`

View file

@ -620,7 +620,7 @@ describe('qwen serve — POST /session/:id/continue', () => {
describe('qwen serve — prompt clientId admission', () => {
// Validates the three real-daemon behaviors that DaemonSessionClient's
// clientId self-heal relies on (see
// docs/superpowers/specs/2026-06-24-daemon-clientid-self-heal-design.md).
// docs/design/2026-06-24-daemon-clientid-self-heal-design.md).
// Model-free: prompt admission (where invalid_client_id is decided) runs
// before any model call, so promptNonBlocking returns 202 on acceptance
// without reaching the (unreachable, fake) model.

View file

@ -10,7 +10,7 @@
* Mirrors Claude Code 2.1.168's `.claude/agents/<name>.md` schema verbatim so
* a user can drop a Claude Code agent file into `.qwen/agents/` and have it
* parse identically. The internal verification source (DL7 / Ig5 / GN / kc /
* P37 / _Y) is documented in `docs/declarative-agents-port.md`.
* P37 / _Y) is documented in `docs/design/declarative-agents-port.md`.
*
* Parsing follows DL7's "lenient" posture: invalid optional fields are dropped
* to undefined rather than thrown the caller layer is responsible for

View file

@ -678,7 +678,7 @@ export class SubagentManager {
}
// Nested CC fields. Safe to round-trip with the eemeli/yaml parser; the
// previous skip-list carve-out is gone (see docs/yaml-parser-replacement.md).
// previous skip-list carve-out is gone (see docs/design/yaml-parser-replacement.md).
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
frontmatter['mcpServers'] = config.mcpServers;
}

View file

@ -202,7 +202,7 @@ function parseSimple(yamlString: string): Record<string, unknown> {
* arbitrarily nested values (e.g. CC-style `mcpServers` / `hooks`) round-trip
* cleanly. The previous hand-rolled formatter only walked one level of
* nesting and emitted `[object Object]` for anything deeper, corrupting the
* file on save see `docs/yaml-parser-replacement.md` for the audit.
* file on save see `docs/design/yaml-parser-replacement.md` for the audit.
*
* `lineWidth: 0` disables automatic line wrapping so multi-line strings are
* preserved as-is, matching the stable-output posture the test suite assumes.