From 067cfbba622a01490cea07d758877647f52b2f68 Mon Sep 17 00:00:00 2001 From: Dragon <52599892+DragonnZhang@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:05:05 +0800 Subject: [PATCH] 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 Co-authored-by: Claude Opus 4.8 --- ...07-01-channel-lifecycle-status-umbrella.md | 42 ----- .qwen/skills/feat-dev/SKILL.md | 4 +- AGENTS.md | 13 +- ...26-05-21-memory-pressure-monitor-design.md | 0 .../2026-05-26-daemon-logger-design.md | 0 ...6-05-27-daemon-workspace-service-design.md | 0 ...6-06-12-session-shell-permission-policy.md | 0 ...06-13-file-history-snapshot-persistence.md | 0 .../2026-06-15-simulated-sed-file-history.md | 0 .../2026-06-23-fix-conflicts-command.md | 0 ...-06-24-daemon-clientid-self-heal-design.md | 0 ...-06-24-stream-inactivity-timeout-design.md | 0 ...2026-06-30-unified-reasoning-effort-cli.md | 40 ++--- ...07-01-channel-lifecycle-status-adapters.md | 40 ++--- ...07-01-channel-lifecycle-status-umbrella.md | 42 +++++ ...annel-p0-identity-task-lifecycle-design.md | 0 ...-07-05-large-frame-handling-measurement.md | 0 .../2026-07-06-session-start-profiler.md | 0 .../design/daemon-extension-at-mention.md | 0 .../daemon-multi-workspace-phase1-registry.md | 0 ...daemon-multi-workspace-phase2a-sessions.md | 0 docs/{ => design}/declarative-agents-port.md | 2 +- .../design/prompt-queue-backpressure.md | 0 .../design/serve-server-final-split.md | 0 {.qwen => docs}/design/serve-server-split.md | 0 .../design/tui-spacing-density-pr1.md | 12 +- .../design/tui-user-message-half-line-pr2.md | 14 +- .../design/webshell-mention-icon-chips.md | 0 docs/{ => design}/yaml-parser-replacement.md | 26 +-- .../2025-06-03-stats-dashboard-redesign.md | 157 ++++++++++++++---- .../plans/2026-05-15-worktree-phase-c.md | 0 .../plans/2026-05-26-daemon-logger.md | 4 +- .../2026-05-27-daemon-workspace-service.md | 2 +- .../plans/2026-05-28-computer-use-built-in.md | 0 .../plans/2026-06-30-channel-loop.md | 0 ...07-01-channel-lifecycle-status-adapters.md | 14 +- ...07-01-channel-lifecycle-status-umbrella.md | 0 ...7-01-channel-p0-identity-task-lifecycle.md | 128 +++++++------- .../cli/qwen-serve-routes.test.ts | 2 +- .../src/subagents/agent-frontmatter-schema.ts | 2 +- .../core/src/subagents/subagent-manager.ts | 2 +- packages/core/src/utils/yaml-parser.ts | 2 +- 42 files changed, 324 insertions(+), 224 deletions(-) delete mode 100644 .qwen/design/2026-07-01-channel-lifecycle-status-umbrella.md rename {.qwen => docs}/design/2026-05-21-memory-pressure-monitor-design.md (100%) rename docs/{superpowers/specs => design}/2026-05-26-daemon-logger-design.md (100%) rename docs/{superpowers/specs => design}/2026-05-27-daemon-workspace-service-design.md (100%) rename {.qwen => docs}/design/2026-06-12-session-shell-permission-policy.md (100%) rename {.qwen => docs}/design/2026-06-13-file-history-snapshot-persistence.md (100%) rename {.qwen => docs}/design/2026-06-15-simulated-sed-file-history.md (100%) rename {.qwen => docs}/design/2026-06-23-fix-conflicts-command.md (100%) rename docs/{superpowers/specs => design}/2026-06-24-daemon-clientid-self-heal-design.md (100%) rename docs/{superpowers/specs => design}/2026-06-24-stream-inactivity-timeout-design.md (100%) rename {.qwen => docs}/design/2026-06-30-unified-reasoning-effort-cli.md (76%) rename {.qwen => docs}/design/2026-07-01-channel-lifecycle-status-adapters.md (67%) create mode 100644 docs/design/2026-07-01-channel-lifecycle-status-umbrella.md rename docs/{superpowers/specs => design}/2026-07-01-channel-p0-identity-task-lifecycle-design.md (100%) rename {.qwen => docs}/design/2026-07-05-large-frame-handling-measurement.md (100%) rename {.qwen => docs}/design/2026-07-06-session-start-profiler.md (100%) rename {.qwen => docs}/design/daemon-extension-at-mention.md (100%) rename {.qwen => docs}/design/daemon-multi-workspace-phase1-registry.md (100%) rename {.qwen => docs}/design/daemon-multi-workspace-phase2a-sessions.md (100%) rename docs/{ => design}/declarative-agents-port.md (99%) rename {.qwen => docs}/design/prompt-queue-backpressure.md (100%) rename {.qwen => docs}/design/serve-server-final-split.md (100%) rename {.qwen => docs}/design/serve-server-split.md (100%) rename {.qwen => docs}/design/tui-spacing-density-pr1.md (71%) rename {.qwen => docs}/design/tui-user-message-half-line-pr2.md (80%) rename {.qwen => docs}/design/webshell-mention-icon-chips.md (100%) rename docs/{ => design}/yaml-parser-replacement.md (96%) rename {.qwen => docs}/plans/2025-06-03-stats-dashboard-redesign.md (93%) rename docs/{superpowers => }/plans/2026-05-15-worktree-phase-c.md (100%) rename docs/{superpowers => }/plans/2026-05-26-daemon-logger.md (99%) rename docs/{superpowers => }/plans/2026-05-27-daemon-workspace-service.md (99%) rename docs/{superpowers => }/plans/2026-05-28-computer-use-built-in.md (100%) rename docs/{superpowers => }/plans/2026-06-30-channel-loop.md (100%) rename {.qwen => docs}/plans/2026-07-01-channel-lifecycle-status-adapters.md (99%) rename {.qwen => docs}/plans/2026-07-01-channel-lifecycle-status-umbrella.md (100%) rename docs/{superpowers => }/plans/2026-07-01-channel-p0-identity-task-lifecycle.md (88%) diff --git a/.qwen/design/2026-07-01-channel-lifecycle-status-umbrella.md b/.qwen/design/2026-07-01-channel-lifecycle-status-umbrella.md deleted file mode 100644 index f362fd74a4..0000000000 --- a/.qwen/design/2026-07-01-channel-lifecycle-status-umbrella.md +++ /dev/null @@ -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. diff --git a/.qwen/skills/feat-dev/SKILL.md b/.qwen/skills/feat-dev/SKILL.md index d5b31da32e..69245ac794 100644 --- a/.qwen/skills/feat-dev/SKILL.md +++ b/.qwen/skills/feat-dev/SKILL.md @@ -14,9 +14,9 @@ feeds the next. ## Artifact Paths -Use `.qwen/` paths for planning artifacts: +Use these paths for planning artifacts: -- `.qwen/design/.md` +- `docs/design/.md` - `.qwen/e2e-tests/.md` ## Phase 1: Investigate diff --git a/AGENTS.md b/AGENTS.md index 79e3792bb9..69276cc828 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 | diff --git a/.qwen/design/2026-05-21-memory-pressure-monitor-design.md b/docs/design/2026-05-21-memory-pressure-monitor-design.md similarity index 100% rename from .qwen/design/2026-05-21-memory-pressure-monitor-design.md rename to docs/design/2026-05-21-memory-pressure-monitor-design.md diff --git a/docs/superpowers/specs/2026-05-26-daemon-logger-design.md b/docs/design/2026-05-26-daemon-logger-design.md similarity index 100% rename from docs/superpowers/specs/2026-05-26-daemon-logger-design.md rename to docs/design/2026-05-26-daemon-logger-design.md diff --git a/docs/superpowers/specs/2026-05-27-daemon-workspace-service-design.md b/docs/design/2026-05-27-daemon-workspace-service-design.md similarity index 100% rename from docs/superpowers/specs/2026-05-27-daemon-workspace-service-design.md rename to docs/design/2026-05-27-daemon-workspace-service-design.md diff --git a/.qwen/design/2026-06-12-session-shell-permission-policy.md b/docs/design/2026-06-12-session-shell-permission-policy.md similarity index 100% rename from .qwen/design/2026-06-12-session-shell-permission-policy.md rename to docs/design/2026-06-12-session-shell-permission-policy.md diff --git a/.qwen/design/2026-06-13-file-history-snapshot-persistence.md b/docs/design/2026-06-13-file-history-snapshot-persistence.md similarity index 100% rename from .qwen/design/2026-06-13-file-history-snapshot-persistence.md rename to docs/design/2026-06-13-file-history-snapshot-persistence.md diff --git a/.qwen/design/2026-06-15-simulated-sed-file-history.md b/docs/design/2026-06-15-simulated-sed-file-history.md similarity index 100% rename from .qwen/design/2026-06-15-simulated-sed-file-history.md rename to docs/design/2026-06-15-simulated-sed-file-history.md diff --git a/.qwen/design/2026-06-23-fix-conflicts-command.md b/docs/design/2026-06-23-fix-conflicts-command.md similarity index 100% rename from .qwen/design/2026-06-23-fix-conflicts-command.md rename to docs/design/2026-06-23-fix-conflicts-command.md diff --git a/docs/superpowers/specs/2026-06-24-daemon-clientid-self-heal-design.md b/docs/design/2026-06-24-daemon-clientid-self-heal-design.md similarity index 100% rename from docs/superpowers/specs/2026-06-24-daemon-clientid-self-heal-design.md rename to docs/design/2026-06-24-daemon-clientid-self-heal-design.md diff --git a/docs/superpowers/specs/2026-06-24-stream-inactivity-timeout-design.md b/docs/design/2026-06-24-stream-inactivity-timeout-design.md similarity index 100% rename from docs/superpowers/specs/2026-06-24-stream-inactivity-timeout-design.md rename to docs/design/2026-06-24-stream-inactivity-timeout-design.md diff --git a/.qwen/design/2026-06-30-unified-reasoning-effort-cli.md b/docs/design/2026-06-30-unified-reasoning-effort-cli.md similarity index 76% rename from .qwen/design/2026-06-30-unified-reasoning-effort-cli.md rename to docs/design/2026-06-30-unified-reasoning-effort-cli.md index b294c8e6fe..f10f9aa517 100644 --- a/.qwen/design/2026-06-30-unified-reasoning-effort-cli.md +++ b/docs/design/2026-06-30-unified-reasoning-effort-cli.md @@ -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, diff --git a/.qwen/design/2026-07-01-channel-lifecycle-status-adapters.md b/docs/design/2026-07-01-channel-lifecycle-status-adapters.md similarity index 67% rename from .qwen/design/2026-07-01-channel-lifecycle-status-adapters.md rename to docs/design/2026-07-01-channel-lifecycle-status-adapters.md index fe57fb1980..df89e223e3 100644 --- a/.qwen/design/2026-07-01-channel-lifecycle-status-adapters.md +++ b/docs/design/2026-07-01-channel-lifecycle-status-adapters.md @@ -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 diff --git a/docs/design/2026-07-01-channel-lifecycle-status-umbrella.md b/docs/design/2026-07-01-channel-lifecycle-status-umbrella.md new file mode 100644 index 0000000000..ea65312f74 --- /dev/null +++ b/docs/design/2026-07-01-channel-lifecycle-status-umbrella.md @@ -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. diff --git a/docs/superpowers/specs/2026-07-01-channel-p0-identity-task-lifecycle-design.md b/docs/design/2026-07-01-channel-p0-identity-task-lifecycle-design.md similarity index 100% rename from docs/superpowers/specs/2026-07-01-channel-p0-identity-task-lifecycle-design.md rename to docs/design/2026-07-01-channel-p0-identity-task-lifecycle-design.md diff --git a/.qwen/design/2026-07-05-large-frame-handling-measurement.md b/docs/design/2026-07-05-large-frame-handling-measurement.md similarity index 100% rename from .qwen/design/2026-07-05-large-frame-handling-measurement.md rename to docs/design/2026-07-05-large-frame-handling-measurement.md diff --git a/.qwen/design/2026-07-06-session-start-profiler.md b/docs/design/2026-07-06-session-start-profiler.md similarity index 100% rename from .qwen/design/2026-07-06-session-start-profiler.md rename to docs/design/2026-07-06-session-start-profiler.md diff --git a/.qwen/design/daemon-extension-at-mention.md b/docs/design/daemon-extension-at-mention.md similarity index 100% rename from .qwen/design/daemon-extension-at-mention.md rename to docs/design/daemon-extension-at-mention.md diff --git a/.qwen/design/daemon-multi-workspace-phase1-registry.md b/docs/design/daemon-multi-workspace-phase1-registry.md similarity index 100% rename from .qwen/design/daemon-multi-workspace-phase1-registry.md rename to docs/design/daemon-multi-workspace-phase1-registry.md diff --git a/.qwen/design/daemon-multi-workspace-phase2a-sessions.md b/docs/design/daemon-multi-workspace-phase2a-sessions.md similarity index 100% rename from .qwen/design/daemon-multi-workspace-phase2a-sessions.md rename to docs/design/daemon-multi-workspace-phase2a-sessions.md diff --git a/docs/declarative-agents-port.md b/docs/design/declarative-agents-port.md similarity index 99% rename from docs/declarative-agents-port.md rename to docs/design/declarative-agents-port.md index ed357f06eb..4177af19cc 100644 --- a/docs/declarative-agents-port.md +++ b/docs/design/declarative-agents-port.md @@ -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. diff --git a/.qwen/design/prompt-queue-backpressure.md b/docs/design/prompt-queue-backpressure.md similarity index 100% rename from .qwen/design/prompt-queue-backpressure.md rename to docs/design/prompt-queue-backpressure.md diff --git a/.qwen/design/serve-server-final-split.md b/docs/design/serve-server-final-split.md similarity index 100% rename from .qwen/design/serve-server-final-split.md rename to docs/design/serve-server-final-split.md diff --git a/.qwen/design/serve-server-split.md b/docs/design/serve-server-split.md similarity index 100% rename from .qwen/design/serve-server-split.md rename to docs/design/serve-server-split.md diff --git a/.qwen/design/tui-spacing-density-pr1.md b/docs/design/tui-spacing-density-pr1.md similarity index 71% rename from .qwen/design/tui-spacing-density-pr1.md rename to docs/design/tui-spacing-density-pr1.md index dcf7993ebc..ff04ceeae7 100644 --- a/.qwen/design/tui-spacing-density-pr1.md +++ b/docs/design/tui-spacing-density-pr1.md @@ -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. diff --git a/.qwen/design/tui-user-message-half-line-pr2.md b/docs/design/tui-user-message-half-line-pr2.md similarity index 80% rename from .qwen/design/tui-user-message-half-line-pr2.md rename to docs/design/tui-user-message-half-line-pr2.md index 70817e2ab5..726cc71219 100644 --- a/.qwen/design/tui-user-message-half-line-pr2.md +++ b/docs/design/tui-user-message-half-line-pr2.md @@ -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 diff --git a/.qwen/design/webshell-mention-icon-chips.md b/docs/design/webshell-mention-icon-chips.md similarity index 100% rename from .qwen/design/webshell-mention-icon-chips.md rename to docs/design/webshell-mention-icon-chips.md diff --git a/docs/yaml-parser-replacement.md b/docs/design/yaml-parser-replacement.md similarity index 96% rename from docs/yaml-parser-replacement.md rename to docs/design/yaml-parser-replacement.md index 472c61d5e5..38199d9619 100644 --- a/docs/yaml-parser-replacement.md +++ b/docs/design/yaml-parser-replacement.md @@ -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 ~85–121 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 ~85–121 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`. diff --git a/.qwen/plans/2025-06-03-stats-dashboard-redesign.md b/docs/plans/2025-06-03-stats-dashboard-redesign.md similarity index 93% rename from .qwen/plans/2025-06-03-stats-dashboard-redesign.md rename to docs/plans/2025-06-03-stats-dashboard-redesign.md index ca20961816..6b0eaddcad 100644 --- a/.qwen/plans/2025-06-03-stats-dashboard-redesign.md +++ b/docs/plans/2025-06-03-stats-dashboard-redesign.md @@ -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; + 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 { +function makeRecord( + overrides: Partial = {}, +): UsageSummaryRecord { return { version: 1, sessionId: 's1', @@ -231,7 +282,10 @@ function makeRecord(overrides: Partial = {}): 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(); + const orig = + await importOriginal(); 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 diff --git a/docs/superpowers/plans/2026-05-15-worktree-phase-c.md b/docs/plans/2026-05-15-worktree-phase-c.md similarity index 100% rename from docs/superpowers/plans/2026-05-15-worktree-phase-c.md rename to docs/plans/2026-05-15-worktree-phase-c.md diff --git a/docs/superpowers/plans/2026-05-26-daemon-logger.md b/docs/plans/2026-05-26-daemon-logger.md similarity index 99% rename from docs/superpowers/plans/2026-05-26-daemon-logger.md rename to docs/plans/2026-05-26-daemon-logger.md index b98afa80e2..748c0bb30d 100644 --- a/docs/superpowers/plans/2026-05-26-daemon-logger.md +++ b/docs/plans/2026-05-26-daemon-logger.md @@ -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/ && npx vitest run `. @@ -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). --- diff --git a/docs/superpowers/plans/2026-05-27-daemon-workspace-service.md b/docs/plans/2026-05-27-daemon-workspace-service.md similarity index 99% rename from docs/superpowers/plans/2026-05-27-daemon-workspace-service.md rename to docs/plans/2026-05-27-daemon-workspace-service.md index 9cebca54cc..63cea4827e 100644 --- a/docs/superpowers/plans/2026-05-27-daemon-workspace-service.md +++ b/docs/plans/2026-05-27-daemon-workspace-service.md @@ -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` --- diff --git a/docs/superpowers/plans/2026-05-28-computer-use-built-in.md b/docs/plans/2026-05-28-computer-use-built-in.md similarity index 100% rename from docs/superpowers/plans/2026-05-28-computer-use-built-in.md rename to docs/plans/2026-05-28-computer-use-built-in.md diff --git a/docs/superpowers/plans/2026-06-30-channel-loop.md b/docs/plans/2026-06-30-channel-loop.md similarity index 100% rename from docs/superpowers/plans/2026-06-30-channel-loop.md rename to docs/plans/2026-06-30-channel-loop.md diff --git a/.qwen/plans/2026-07-01-channel-lifecycle-status-adapters.md b/docs/plans/2026-07-01-channel-lifecycle-status-adapters.md similarity index 99% rename from .qwen/plans/2026-07-01-channel-lifecycle-status-adapters.md rename to docs/plans/2026-07-01-channel-lifecycle-status-adapters.md index b58bed7f41..6cb1045dcf 100644 --- a/.qwen/plans/2026-07-01-channel-lifecycle-status-adapters.md +++ b/docs/plans/2026-07-01-channel-lifecycle-status-adapters.md @@ -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}*`; diff --git a/.qwen/plans/2026-07-01-channel-lifecycle-status-umbrella.md b/docs/plans/2026-07-01-channel-lifecycle-status-umbrella.md similarity index 100% rename from .qwen/plans/2026-07-01-channel-lifecycle-status-umbrella.md rename to docs/plans/2026-07-01-channel-lifecycle-status-umbrella.md diff --git a/docs/superpowers/plans/2026-07-01-channel-p0-identity-task-lifecycle.md b/docs/plans/2026-07-01-channel-p0-identity-task-lifecycle.md similarity index 88% rename from docs/superpowers/plans/2026-07-01-channel-p0-identity-task-lifecycle.md rename to docs/plans/2026-07-01-channel-p0-identity-task-lifecycle.md index 225ff470fe..8f9ad72a6d 100644 --- a/docs/superpowers/plans/2026-07-01-channel-p0-identity-task-lifecycle.md +++ b/docs/plans/2026-07-01-channel-p0-identity-task-lifecycle.md @@ -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` diff --git a/integration-tests/cli/qwen-serve-routes.test.ts b/integration-tests/cli/qwen-serve-routes.test.ts index 1b21764121..a48aa1bd2b 100644 --- a/integration-tests/cli/qwen-serve-routes.test.ts +++ b/integration-tests/cli/qwen-serve-routes.test.ts @@ -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. diff --git a/packages/core/src/subagents/agent-frontmatter-schema.ts b/packages/core/src/subagents/agent-frontmatter-schema.ts index 2db8999ccc..4305c3fb4f 100644 --- a/packages/core/src/subagents/agent-frontmatter-schema.ts +++ b/packages/core/src/subagents/agent-frontmatter-schema.ts @@ -10,7 +10,7 @@ * Mirrors Claude Code 2.1.168's `.claude/agents/.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 diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index 13039891f3..62319036e5 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -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; } diff --git a/packages/core/src/utils/yaml-parser.ts b/packages/core/src/utils/yaml-parser.ts index ead17bbb65..fecad37ce5 100644 --- a/packages/core/src/utils/yaml-parser.ts +++ b/packages/core/src/utils/yaml-parser.ts @@ -202,7 +202,7 @@ function parseSimple(yamlString: string): Record { * 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.