qwen-code/docs
顾盼 5c54a2cf8e
feat(core): declarative agent frontmatter v1 — permissionMode bridge + maxTurns wiring + color allowlist (CC 2.1.168 parity) (#4842)
* docs: add declarative agents port design doc for #4821

* feat(core): add declarative agent frontmatter schema constants and 9 new fields

Adds agent-frontmatter-schema.ts as the single source of truth for the CC 2.1.168
declarative-agent enum constants (EFFORT_VALUES, PERMISSION_MODE_VALUES,
MEMORY_VALUES, ISOLATION_VALUES, COLOR_VALUES) and lenient parsers (parseEffort,
parseMaxTurns, parseBackground, parseStringOrArray) that mirror DL7's warn-and-drop
posture. Also adds a permissionModeToApprovalMode bridge to map CC's permission
modes onto qwen-code's existing approvalMode semantics.

Extends SubagentConfig with 9 optional fields carried verbatim from CC frontmatter:
permissionMode, effort, maxTurns, skills, initialPrompt, memory, isolation,
mcpServers, hooks. Runtime semantics for the metadata-only fields are deferred
to follow-up PRs; this lands the data carrier.

Refs #4821 #4721 #4732

* feat(core): parse and serialize 9 new declarative agent frontmatter fields

Extends parseSubagentContent and serializeSubagent to round-trip the 9 CC
2.1.168 fields previously added to the SubagentConfig type. Parsing follows
DL7's lenient warn-and-drop posture (vs the existing strict throw posture
used for approvalMode), so a Claude Code agent file with an invalid optional
field still parses with that field dropped — matching CC behavior so users
can drop CC agent files into .qwen/agents/ unchanged.

Adds a permissionMode → approvalMode bridge: when frontmatter has
permissionMode but no approvalMode, the bridge resolves the approvalMode at
parse time using the same mapping as claude-converter.ts. If both are set,
approvalMode wins (already-explicit values take precedence over inferred
ones).

Tests:
- 22 new parser cases covering happy paths, invalid drops, permissionMode
  bridge precedence, color allowlist (with auto sentinel preserved), and
  loose-validation passthrough for mcpServers / hooks.
- 9 new serializer cases asserting round-trip and omit-when-unset behavior.

Refs #4821 #4721 #4732

* feat(core): promote top-level maxTurns to runConfig.max_turns at convert time

Wires the new top-level `maxTurns` field into the existing runtime configuration
pipeline so the value declared in agent frontmatter actually limits the agent's
turn budget at run time. When both the top-level `maxTurns` and the legacy
nested `runConfig.max_turns` are set, the top-level field wins (more specific
and matches the CC frontmatter shape upstream agent files use). When only the
nested field is set, behavior is unchanged.

Refs #4821 #4721 #4732

* refactor(core): use shared permissionMode bridge and parseStringOrArray in claude-converter

Replaces the inline claudeToQwenMode table and the local parseStringOrArray
helper in claude-converter.ts with the shared exports from
agent-frontmatter-schema.ts. One source of truth for the CC ↔ qwen mapping
keeps the two import paths (.qwen/agents/*.md frontmatter and Claude plugin
import) in sync — when the upstream CC schema changes, only one table needs
updating.

Adds explicit tests for the bridge mapping (six known modes + unknown
fallback). The fallback now happens at the call site rather than inside the
table, but the observable behavior is unchanged.

Refs #4821

* docs(subagents): document CC-compatible frontmatter fields

Adds a Claude Code Compatibility Fields section to the user-facing subagents
reference, covering the 9 new frontmatter fields landed in this PR. The intent
is for users with existing Claude Code agents to know they can drop the files
into `.qwen/agents/` and have them parse identically.

Refs #4821

* refactor(core): reuse parseBackground in declarative agent loader

Replaces the inline boolean-or-string lenient parse for the background field
with the shared parseBackground util added in the schema module. Same observable
behavior; one fewer place to keep in sync when the upstream shape changes.

Refs #4821

* fix(core): address adversarial review of declarative agents PR

Four self-review findings caught and fixed before opening the PR:

1. **mcpServers/hooks round-trip claim was broken.** The local yaml-parser only
   formats one level of nesting, so serializing an agent with nested mcpServers
   or hooks would mangle the value into '[object Object]'. The fields are still
   carried verbatim in memory (read path is fine), but the serializer no longer
   emits them — losing the field through '/agents' edits is strictly safer than
   corrupting it. Documented the limitation in docs/users/features/sub-agents.md.

2. **approvalMode throw vs permissionMode lenience asymmetry.** The pre-existing
   approvalMode parser threw on invalid values, killing the entire agent file.
   With the new permissionMode bridge, a CC-imported file with 'permissionMode:
   bypassPermissions' + 'approvalMode: tpyo' would reject the file instead of
   dropping the typo and using the bridge. Demoted approvalMode to the same
   DL7-parity warn-and-drop posture all the new fields use; the bridge now runs
   when approvalMode is invalid.

3. **parseEffort missed numeric strings.** CC's DL7 falls back to parseInt for
   non-enum effort strings so 'effort: "5"' (quoted YAML) round-trips like
   'effort: 5'. Added the same fallback and tests for floats / partial numeric
   strings / valid numeric strings.

4. **convertAgentFiles stripped 6/9 of the new fields.** The Claude plugin
   import path read frontmatter into a fixed ClaudeAgentConfig shape, so
   'effort', 'maxTurns', 'initialPrompt', 'memory', 'isolation', 'mcpServers',
   and 'background' were silently dropped when installing a CC plugin agent.
   Build the rewritten frontmatter from the original keys first, then overlay
   the converter's transformations for the keys it owns — unknown CC fields
   now passthrough verbatim, future-proofing against later CC additions.

Refs #4821 #4721 #4732

* fix(core): round-2 self-review fixes for declarative agents PR

Second adversarial review surfaced 5 more findings; all fixed:

1. **convertAgentFiles passthrough corrupted nested objects.** Round-1 fixed
   serializeSubagent to skip emitting mcpServers/hooks (the local yaml-parser
   collapses nested values to '[object Object]'). The plugin-import passthrough
   I added to claude-converter.ts had the same mangle bug — a CC plugin agent
   with nested mcpServers got written to disk as 'mcpServers:\n  - [object
   Object]'. Now skips both fields via a NESTED_FIELDS_NOT_ROUND_TRIPPABLE set.

2. **parseEffort accepted 0 and negative integers.** Docs say 'positive integer'
   and parseMaxTurns already rejects <= 0. parseEffort didn't, accepting
   '0' / '-5' / 0 / -1 as valid efforts. Aligned both helpers and added tests
   for the boundary values.

3. **Function name collision: permissionModeToApprovalMode.** packages/core/src/
   tools/agent/agent.ts has a module-private permissionModeToApprovalMode that
   maps the qwen PermissionMode enum to ApprovalMode enum (different domain
   entirely). My new exported helper used the same name; IDE auto-import would
   silently return undefined for every qwen enum value. Renamed the export to
   claudePermissionModeToApprovalMode and updated 4 callers.

4. **color: 'auto' round-trip asymmetry.** Round-1 added a test pinning that
   the parser preserves the legacy 'auto' sentinel. But serializeSubagent
   already had a pre-existing 'skip emit when auto' branch, so a parse →
   serialize → parse cycle dropped the sentinel. The CLI helpers
   (shouldShowColor / getColorForDisplay) already treat 'auto' identically to
   undefined, so normalizing 'auto' to undefined at parse time has no
   downstream effect AND makes round-trip cleanly idempotent.

5. **Converter approvalMode precedence diverged from loader.** When a CC source
   file had both 'permissionMode: bypassPermissions' AND 'approvalMode: default',
   the convertClaudeAgentConfig path wrote 'approvalMode: yolo' (bridge wins).
   The loader's rule is 'approvalMode wins over bridge'. Extended
   ClaudeAgentConfig with an approvalMode field and gated the bridge emit on
   'source approvalMode is unset', aligning convert and load precedence.

Refs #4821 #4721 #4732

* fix(core): round-3 self-review fixes — drift, API hygiene, converter contract

Three quality findings from the round-3 adversarial pass (after rounds 1+2
fixed 9 issues each). All low-severity but cheap; the runtime was correct,
the file-on-disk + public-API surface needed tightening.

1. **Stale runConfig.max_turns duplicated on every write.** Top-level maxTurns
   was promoted in 9b903ee41 but the serializer still emitted runConfig.max_turns
   verbatim. A file with both fields kept both indefinitely. The runtime
   already prefers top-level (so behavior is correct), but a third-party tool
   or future viewer reading the legacy nested field would act on stale data.
   Prune nested max_turns from the serialized runConfig when top-level
   maxTurns is set; drop the runConfig block entirely if pruning leaves it
   empty. Three new tests pin the prune, the drop-empty-block path, and the
   no-top-level fallback.

2. **14 internal symbols leaked into the public package API.** The PR
   re-exported every helper from agent-frontmatter-schema.ts via
   subagents/index.ts. grep against packages/cli + packages/vscode-ide-companion
   finds zero cross-package consumers — both internal users (subagent-manager,
   claude-converter) already import directly from the schema file. Locking
   constant names like EFFORT_VALUES and parseEffort in the public API would
   constrain follow-up PRs (e.g. when js-yaml lands and the schema shape
   changes). Removed the entire export block; left a comment explaining the
   choice so a future PR knows the bar for re-introducing it.

3. **convertClaudeAgentConfig silently dropped a standalone approvalMode.**
   Round-2 gated the permissionMode bridge on !claudeAgent.approvalMode for
   precedence parity with the loader, but never added the explicit copy for
   approvalMode-only callers. Result: caller passes {approvalMode: 'plan'} →
   output has no approvalMode at all. The in-tree convertAgentFiles path
   recovered via the unowned-key passthrough, but exported direct-call surface
   needs the explicit copy. Two new tests pin both the standalone case and
   the precedence-when-both case.

Also: pinned yaml-parser limitation tests (added in the round-3 e2e
independent check) document that the lightweight parser cannot round-trip
nested mcpServers/hooks; documentation + types comments updated to call out
the carve-out so users know the field works only for flat forms until
js-yaml lands.

Refs #4821 #4721 #4732

* refactor(core): shrink declarative agents PR to vertically-sliced v1

Reduces this PR to ship only the fields that have an end-to-end runtime path
today: permissionMode (bridges to existing approvalMode), maxTurns (wires
into runConfig.max_turns), and a tightened color allowlist. Everything else
gets deferred to follow-up PRs that first land the prerequisite infra they
need.

Removed:
- effort (no model-layer effort param in qwen providers)
- mcpServers / hooks (need nested-aware YAML parser — yaml-parser.ts is
  hand-rolled and only handles 1 level)
- memory (qwen's auto-memory has no user/project/local scope distinction)
- isolation (workflow PR #4732 owns the runtime)
- initialPrompt (needs --agent CLI flag, no main-session-agent infra)
- skills (needs SkillManager consumption of config.skills)

This drops 7 fields from SubagentConfig + types.ts, deletes their parser /
serializer / test code, deletes the unused enum constants and helpers from
agent-frontmatter-schema.ts (EFFORT_VALUES, EFFORT_ALIASES, MEMORY_VALUES,
ISOLATION_VALUES, parseEffort, isMemory, isIsolation), and trims the user
docs to the supported field set with a pointer to the design doc for the
deferred fields.

Why ship the design doc alongside such a narrow v1: the full
reverse-engineering record in docs/declarative-agents-port.md (DL7/Ig5/GN/_Y
constants, error messages, schema parity decisions, coordination matrix with
workflow PR #4732) stays load-bearing for the follow-up PRs. A status table
at the top discloses what's deferred and why.

Net: -559 LOC across 7 files. Final PR is permissionMode bridge + maxTurns
wiring + color allowlist + design reference doc + the yaml-parser
nested-limitation pin tests added during round-3 review.

Refs #4821 #4721 #4732

* refactor(core): revert round-X residuals to hit v1's actual minimum LOC

The previous shrink left round-1/2/3 fixes in place that were originally
for the wider 9-field scope; with the carry-only fields gone they're no
longer needed. Reverting them all back to pre-PR behaviour:

- approvalMode strict throw (round-1 demoted to lenient; defer the
  symmetry fix to a separate PR if wanted)
- runConfig.max_turns prune on serialize (round-3 cleanup; defer)
- color: 'auto' normalised to undefined (round-2 round-trip fix; defer)
- claude-converter NESTED_FIELDS_NOT_ROUND_TRIPPABLE + CONVERTER_OWNED_KEYS
  + passthrough loop (round-1/2 — all for the deleted carry-only fields)
- ClaudeAgentConfig.approvalMode + precedence gating (round-3)
- parseBackground in shared schema module (the dedup wasn't pulling weight)
- parseStringOrArray in shared schema module (same)

The shared bridge claudePermissionModeToApprovalMode + the disambiguated
name stay — converter and loader use the same map and the name collision
risk is real.

Net: -336 LOC vs the prior shrink commit.

* docs: sync user-facing docs to actual v1 behaviour

The previous shrink left two stale claims in docs/users/features/sub-agents.md
that referenced behaviour reverted in the round-X-revert commit:

- maxTurns row claimed 'the legacy nested value is pruned from the on-disk
  file on save to avoid two sources of truth' — the prune was reverted
- color row claimed 'auto is accepted on read and normalised to undefined
  for round-trip parity' — the normalisation was reverted; auto is preserved
  as-is for backward compat

Updated both rows to match what the parser actually does now.

The design doc (docs/declarative-agents-port.md) is unchanged: its top-line
disclaimer already labels the body as reference material for follow-up PRs,
so the references to the wider plan (D7 bridge, P1-P4 phases) are
accurate-as-reference even though some details are deferred.

* fix(core): use Map.get in claudePermissionModeToApprovalMode (prototype-chain footgun)

The PERMISSION_MODE_TO_APPROVAL_MODE table was a plain object accessed with
`[key]`. Calling claudePermissionModeToApprovalMode('__proto__') walked the
prototype chain and returned Object.prototype — a non-string value that
violated the declared return type. The current loader path was safe because
isPermissionMode() filtered prototype-key strings before the bridge ran, but
the exported function itself was a latent footgun for any future caller
that didn't know to pre-filter.

Switched the lookup table to a Map so prototype keys cannot be reached.
Added explicit tests for __proto__, constructor, hasOwnProperty, toString.

Refs #4821

* fix(core): address round-2 review on declarative agents PR #4842

Two findings:

1. `serializeSubagent` wrote both `permissionMode` and the bridge-derived
   `approvalMode` into the same frontmatter block. On the next load the parser
   takes `approvalMode` (explicit wins over bridge), so `permissionMode`
   silently became dead frontmatter — a user editing it later in the file
   would be ignored. Skip the `permissionMode` emit when `approvalMode` is
   also being written; `permissionMode` still round-trips when it's the
   user's only intent. Two new tests pin both branches.

2. `subagents/index.ts` had a NOTE comment referencing `EFFORT_VALUES` and
   `parseEffort` as example schema helpers that intentionally aren't
   re-exported. Those symbols were removed during the v1 scope shrink
   (637b8b70c) and don't exist in the current module. Updated the comment to
   reference `claudePermissionModeToApprovalMode`, `parseMaxTurns`, and
   `isPermissionMode`, which are the actual current contents.

Sibling-drift note: the same dual-emit pattern exists for `maxTurns` vs
`runConfig.max_turns` — round-X revert (70a876d38) explicitly deferred the
`runConfig.max_turns` prune. Not addressed here per that earlier decision.

Refs #4842
2026-06-11 05:03:31 +08:00
..
design feat(telemetry): Phase 3 — qwen-code.subagent span with concurrent isolation (#3731) (#4410) 2026-06-05 17:12:34 +08:00
developers feat(installer): verify release assets + switch public docs to standalone entrypoint (#3855) 2026-06-04 17:23:04 +08:00
e2e-tests feat(worktree): Phase D — startup --worktree flag + symlinkDirectories + PR refs (#4381) 2026-05-27 17:04:51 +08:00
plans feat(core)!: redesign auto-compaction thresholds with three-tier ladder (#4345) 2026-05-25 21:11:08 +08:00
superpowers/plans feat(computer-use): zero-config built-in via open-computer-use MCP (#4590) 2026-05-29 10:39:51 +08:00
users feat(core): declarative agent frontmatter v1 — permissionMode bridge + maxTurns wiring + color allowlist (CC 2.1.168 parity) (#4842) 2026-06-11 05:03:31 +08:00
verification/abort-controller-refactor fix(core): stop AbortSignal listener leak in long sessions (MaxListenersExceededWarning) (#4366) 2026-05-26 14:21:49 +08:00
_meta.ts feat: refactor docs 2025-12-05 10:51:57 +08:00
declarative-agents-port.md feat(core): declarative agent frontmatter v1 — permissionMode bridge + maxTurns wiring + color allowlist (CC 2.1.168 parity) (#4842) 2026-06-11 05:03:31 +08:00
index.md fix: lint issues 2025-12-19 15:52:11 +08:00