Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2bacaf7
feat(goal): let the model propose a Goal the user approves in a dialog
qqqys Aug 26, 2026
e2c77fd
docs: size the goals.modelProposed settings row to the existing table…
qqqys Aug 26, 2026
549975c
fix(cli): localize propose goal tool
qqqys Aug 26, 2026
e367c4b
fix(goal): apply an approved propose_goal proposal when the proposing…
qqqys Aug 27, 2026
ba945f3
Merge remote-tracking branch 'origin/feat/propose-goal-tool' into fea…
qqqys Aug 27, 2026
10c6fab
fix(core): harden proposed goal settlement
qqqys Aug 27, 2026
1e4acf2
fix(core): harden proposed Goal approval
qqqys Aug 27, 2026
dd9cef7
Merge upstream/main into feat/propose-goal-tool
qqqys Aug 29, 2026
751799f
test(goal): pin the propose_goal lifecycle behaviors the review found…
qqqys Aug 29, 2026
733463f
Merge branch 'main' into feat/propose-goal-tool
qwen-code-dev-bot Aug 29, 2026
c69264d
Merge branch 'main' into feat/propose-goal-tool
qwen-code-dev-bot Aug 29, 2026
ebcbb79
Merge branch 'main' into feat/propose-goal-tool
qwen-code-dev-bot Aug 29, 2026
2167bd3
fix(goal): bind a propose_goal approval to the turn that made it
qqqys Aug 30, 2026
808173d
fix(goal): preserve proposal ownership across turns
qqqys Aug 30, 2026
210baa4
Merge upstream/main into feat/propose-goal-tool
qqqys Aug 30, 2026
9d54eab
chore: merge upstream/main into feat/propose-goal-tool
qqqys Sep 1, 2026
b69c986
chore(goal): merge upstream/main into feat/propose-goal-tool
qwen-code-dev-bot Sep 1, 2026
12bb62c
chore: merge upstream/main into feat/propose-goal-tool
qqqys Sep 1, 2026
5c640ce
Merge remote-tracking branch 'upstream/main' into feat/propose-goal-tool
qqqys Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/design/2026-08-25-goal-draft-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,17 @@ The objective is handed over on one line because `parseGoalCommand` splits on wh
- `docs/users/features/goals.md` (commands, how a Goal is judged, writing a good objective, `/goal-draft`), rows in `commands.md`, a pointer from `headless.md`.
- The web-shell Goals dialog placeholder now shows an objective with a check, a guardrail, and a budget in both locales.

## Phase 2: `propose_goal`

A core tool, registered beside `get_goal` / `update_goal` (so never for subagents), only in interactive sessions (`resolveInteractionMode === 'interactive'`) and when `goals.modelProposed` is not `disabled`; ACP and stream-json remain excluded until they have an equivalent turn-boundary settlement path. It reuses the generic `info` confirmation: the objective is in both the invocation description (the one field every host forwards — the Web Shell drops an `info` prompt) and the plain-text prompt, and `requiresUserInteraction()` is `true` so no allow rule, YOLO, or AUTO_EDIT (which auto-approves `info` confirmations) can skip the dialog. Preconditions are checked before the dialog and again in `execute()`, because `/goal` can change the session while the dialog is open: plan mode, untrusted folder, no Goal persistence, and an active Goal all refuse with guidance (an active Goal is never replaced from the tool; the model hands over a `/goal edit` / `/goal set` line instead). A stopped Goal is replaced through `replace` with its expected version; no Goal creates. The tool does not dispatch at all: setting a Goal mid-turn would leave the rest of the proposing turn without a Goal permit (`client.ts` rejects a permit-less continuation while a Goal is active — the first end-to-end run showed exactly that error card). Instead the approval is parked on `Config` (`setPendingGoalProposal`), bound to the proposing turn's `prompt_id`, and the client applies it when the proposing turn truly ends, after queued steer, Stop-hook, and next-speaker continuations finish (`settlePendingGoalProposal` → `applyPendingGoalProposal`); only that turn's own terminal boundary may apply it, and any other frame that finds it parked drops it instead. The runtime's broadcast then renders the Goal card and starts the first Goal turn. An approval still parked when the next real user query starts belongs to a cancelled turn and is discarded; the tool result tells the model to acknowledge in one sentence and stop.

`goals.modelProposed` (`alwaysAsk` | `disabled`, default `alwaysAsk`) sits in `WORKSPACE_RESTRICTED_SETTINGS`, so a repository cannot switch the tool on. An `auto` mode that lets the model skip the dialog (Claude Code's `ask_user: false`) is deliberately not offered.

The skill's hand-off now prefers the tool when it is available and no Goal is active, and keeps the printed `/goal set` line for headless runs, the disabled setting, and the active-Goal case.

## Later phases (not in this change)

- A `propose_goal` core tool with an approval dialog, mirroring Claude Code's `ProposeGoal` + `modelProposedGoals` (read from user/policy settings only), so the skill can offer "Set this goal" instead of a line to paste. `parseGoalCommand` keeps newlines for `set`/`edit`.
- `parseGoalCommand` keeps newlines for `set`/`edit`.
- A deterministic lint on `/goal set` (rules 1–6 above) that warns and points at `/goal-draft`, and a "refine" entry in the web-shell Goals dialog.

## Verification
Expand Down
1 change: 1 addition & 0 deletions docs/users/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ If you are experiencing performance issues with file searching (e.g., with `@` c
| `tools.useRipgrep` | boolean | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` | |
| `tools.useBuiltinRipgrep` | boolean | Use the bundled ripgrep binary. When set to `false`, the system-level `rg` command will be used instead. This setting is only effective when `tools.useRipgrep` is `true`. | `true` | |
| `tools.workflowsEnabled` | boolean | Enable the Workflow tool, which lets the model author and run a script that orchestrates subagents in parallel. Off by default; a run can dispatch many subagents and spend tokens accordingly. | `false` | User, System, and SystemDefaults scopes only; workspace values are ignored. Requires restart: Yes. Env overrides: `QWEN_CODE_ENABLE_WORKFLOWS=1` forces on; `QWEN_CODE_DISABLE_WORKFLOWS=1` forces off (disable wins). |
| `goals.modelProposed` | enum | Controls the `propose_goal` tool, which lets the model propose a session Goal for you to approve: `alwaysAsk` shows every proposal in an approval dialog and nothing is set until you accept it; `"disabled"` removes the tool. A typed `/goal` is unaffected. | `alwaysAsk` | User, System, and SystemDefaults scopes only; workspace values are ignored. Requires restart: Yes. |
| `tools.truncateToolOutputThreshold` | number | Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `25000` | Requires restart: Yes |
| `tools.truncateToolOutputLines` | number | Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. | `1000` | Requires restart: Yes |
| `tools.toolSearch.enabled` | boolean | Load MCP tools on demand via ToolSearch to reduce prompt size. Disable this for models that rely on prefix-based KV caching (e.g. DeepSeek) to keep the prompt prefix stable and maximize cache hit rates. | `true` | Requires restart: Yes |
Expand Down
8 changes: 7 additions & 1 deletion docs/users/features/goals.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,14 @@ Keep it to one objective and roughly under 1,200 characters. `/goal set` and `/g

## Let `/goal-draft` write it

`/goal-draft <what you want done>` is a bundled skill that does the above for you. It checks whether the request is a Goal at all, reads the workspace for the real test and lint commands instead of guessing, asks at most one round of multiple-choice questions when the answer changes the check or the scope, drafts the objective in the format above, runs the self-check, and prints a `/goal set …` line you can run as-is. It never starts the work itself and never sets the Goal on your behalf.
`/goal-draft <what you want done>` is a bundled skill that does the above for you. It checks whether the request is a Goal at all, reads the workspace for the real test and lint commands instead of guessing, asks at most one round of multiple-choice questions when the answer changes the check or the scope, drafts the objective in the format above, runs the self-check, and hands it over: in an interactive session it proposes the objective through the `propose_goal` approval dialog described below, otherwise it prints a `/goal set …` line you can run as-is. It never starts the work itself, and nothing is set without your approval.

Pass an existing objective to tighten it: `/goal-draft all tests pass and the lint is clean`.

### Approve a Goal the model proposes

In an interactive terminal session the model has a `propose_goal` tool. When `/goal-draft` finishes, or when you ask for an outcome that spans several turns, it can propose the objective instead of printing a `/goal set …` line for you to copy. The proposal appears as an approval dialog showing the full objective. Approving it sets the Goal exactly as `/goal set` would, the moment the current turn ends (the model acknowledges and stops; the first Goal turn then starts on its own), and declining sets nothing — the model is told only that the Goal was not set, and must not propose it again. The approval is bound to the turn that asked for it: if that turn is cancelled or otherwise never reaches its end, the approval is dropped rather than applied under a later message or an automated turn. No permission rule or approval mode (including YOLO) skips this dialog, and the tool refuses while another Goal is active, in plan mode, in subagents, and in untrusted folders. It is not available in headless runs, nor yet in Web Shell or other ACP-driven sessions (they do not pass through the turn boundary that applies the approval); there the printed `/goal set` line remains the hand-off.

Turn it off with `goals.modelProposed: "disabled"` in your user settings. Because the setting decides whether the model may ask you to start an autonomous loop, it is honored only from user and system scope; a workspace `.qwen/settings.json` value is ignored with a warning.

The skill is instructed to be read-only, and only its non-mutating tools are auto-approved (`get_goal`, `read_file`, `glob`, `grep_search`). `ask_user_question` is deliberately not auto-approved, so its question dialog is shown before the skill drafts from your answers. Like other bundled skills, a project or personal skill named `goal-draft` overrides it, and `skills.disabled` can turn it off. See [Skills](./skills.md) for how bundled skills are discovered.
Loading
Loading