diff --git a/docs/design/2026-08-25-goal-draft-skill.md b/docs/design/2026-08-25-goal-draft-skill.md new file mode 100644 index 00000000000..15ae50ff384 --- /dev/null +++ b/docs/design/2026-08-25-goal-draft-skill.md @@ -0,0 +1,44 @@ +# goal-draft: a bundled skill that writes verifier-judgeable Goals + +## Problem + +`/goal ` accepts any non-empty string. The objective is judged by an independent verifier that only sees transcript evidence (`goalJudge.ts`, `goal-verifier.ts`): printed text cannot prove that tests passed or files changed, claims about user actions need a real user message, and an objective nobody can evidence keeps the loop running until a limit stops it. Nothing in the product tells users this. The only guidance was the web-shell placeholder `all tests pass and the lint is clean`, which itself has no check attached. + +## Survey + +The closest existing analogues both converge on the same shape: OpenAI's curated `define-goal` skill for Codex `/goal` (one objective string; five questions — what will be true, what evidence proves it, what threshold, what scope, when to stop and ask; at most one clarifying question) and the community `agent-goal-skill` (a fixed Goal / Context / Constraints / Done when / On block block, binary shell-checkable criteria, `` for unknowns). Claude Code's `/goal` documentation asks for "one measurable end state, a stated check, constraints that matter" and a turn/time clause; Claude Code has no goal-writing skill — its `ProposeGoal` tool prompt carries the rules instead (≤500 characters, verifiable from the conversation alone, never widen scope). Ralph-style loops push the stop condition into the harness and rank guardrails; Anthropic's long-running-agent harness note flags "editing tests to pass" as the failure to forbid. Spec/PRD skills (superpowers, ai-dev-tasks, spec-kit, Kiro EARS, BMAD) contribute the interview discipline: batch questions, offer options, mark assumptions instead of blocking, and never start implementing. + +What differs for an autonomous loop versus a PRD: the stop condition must be machine-checkable, there must be a negative stop (budget) and a block protocol, the same agent grades itself so cheap exits must be forbidden, and the whole thing must fit one session's evidence budget. + +## Design + +A bundled skill at `packages/core/src/skills/bundled/goal-draft/SKILL.md`, registered like every bundled skill as `/goal-draft` (model-invocable too). It is instructed to be read-only and auto-approves only the non-mutating tools (`get_goal`, `read_file`, `glob`, `grep_search`); `allowedTools` is an additive grant, so the read-only discipline is enforced by the skill's prose. `ask_user_question` is deliberately not granted: an allow rule for it would override the tool's `'ask'` default session-wide and run it without the question dialog, fabricating a declined-answer result. It never runs the checks, never edits, and cannot call `/goal` (built-in commands are not model-invocable by design). + +Steps: + +0. Decide whether the request is a Goal at all; one-shot tasks and judgement calls are not. +1. `get_goal`; if a Goal is active, offer `edit` versus `set`, never a second concurrent goal. +2. Ground in the workspace: verify named files and find the real test/lint/build commands; never invent paths, mark unknowns ``. +3. At most one round of 1–3 multiple-choice questions, only when the answer changes the check, scope, or budget; if no verification path can be found the skill must ask rather than invent one; headless takes the recommended default and marks `[ASSUMPTION]`. +4. Draft in a fixed contract, one paragraph, English labels: `Outcome:` / `Done when:` (numbered, binary, at least one tool-observable with "paste that line") / `Must not:` / `Budget:` / `On block:` / `Context:`. +5. Self-check nine rules (binary items, a named check, no subjective adjectives, no "after the user confirms", budget or block clause, one outcome, verified context, length, irreversible actions in Must not) and print the objective plus a one-line `/goal set …` the user can run as-is. Stop. + +The labels map onto verifier rules: `Done when` items produce `external_fact` evidence; `Must not` closes the exits the verifier cannot see; `On block` routes decisions to `blockerKind: authority` instead of a "user confirms" completion condition; `Budget` avoids indefinite "insufficient evidence" loops. + +The objective is handed over on one line because `parseGoalCommand` splits on whitespace and re-joins with single spaces, so newlines would be flattened anyway. + +## Scope of this change + +- The skill and its test (`SKILL.test.ts` pins the allowed tools, the step order, the contract labels, the question rules, and the "do not run /goal, do not begin the task" stop). +- `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. + +## 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`. +- 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 + +- `packages/core`: `bundled-skills.integration.test.ts` parses the new SKILL.md; `goal-draft/SKILL.test.ts` (8 tests); `tsc --noEmit` clean; eslint clean. +- Built CLI run headless in a throwaway project with a `test` and `coverage` script: `/goal-draft make the auth tests pass and raise coverage` produced a six-part objective citing the real `node --test test/` and `c8 …` scripts, two `[ASSUMPTION]` notes, and a `/goal set …` line; the project tree was untouched. `/goal-draft clean up the auth module` turned "clean" into an explicit, assumption-tagged observable definition (tests pass, exports referenced, diff confined to `src/auth`) and invited the user to redefine it. diff --git a/docs/users/features/_meta.ts b/docs/users/features/_meta.ts index 45dcae50c52..1acc57debd4 100644 --- a/docs/users/features/_meta.ts +++ b/docs/users/features/_meta.ts @@ -26,5 +26,6 @@ export default { hooks: 'Hooks', 'status-line': 'Status Line', 'scheduled-tasks': 'Scheduled Tasks', + goals: 'Goals', tips: 'Contextual Tips', }; diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md index 6a5a97cb914..713808cff2b 100644 --- a/docs/users/features/commands.md +++ b/docs/users/features/commands.md @@ -117,7 +117,7 @@ Commands for managing AI tools and models. | `/permissions` | Manage permission rules | `/permissions` | | `/agents` | Manage subagents | `/agents manage`, `/agents create` | | `/arena` | Manage Arena sessions | `/arena start`, `/arena stop`, `/arena status`, `/arena select` (alias `choose`) | -| `/goal` | Set a goal — keep working until condition met | `/goal `, `/goal clear` | +| `/goal` | Set a goal — keep working until condition met (see [Goals](./goals.md)) | `/goal `, `/goal clear` | | `/tasks` | List background tasks | `/tasks` | | `/workflows` | Inspect workflow runs; cooperatively pause/resume a background run | `/workflows`, `/workflows `, `/workflows p ` | | `/lsp` | Show LSP server status | `/lsp` | @@ -144,6 +144,7 @@ These commands invoke bundled skills that provide specialized workflows. | `/review` | Multi-agent code review (12 parallel agents at high effort) | `/review`, `/review 123`, `/review 123 --comment`, `/review --effort low` | | `/coordinate` | Coordinate read-only workers and one optional worktree writer | `/coordinate investigate and fix the authentication regression` | | `/loop` | Run a prompt on a recurring schedule | `/loop 5m check the build` | +| `/goal-draft` | Turn a fuzzy intention into a verifiable `/goal` objective | `/goal-draft make the auth tests pass` | | `/simplify` | Review recent changes and apply safe cleanup edits directly | `/simplify`, `/simplify focus on duplication` | | `/qc-helper` | Answer questions about Qwen Code usage and configuration | `/qc-helper how do I configure MCP?` | diff --git a/docs/users/features/goals.md b/docs/users/features/goals.md new file mode 100644 index 00000000000..8c2203bd553 --- /dev/null +++ b/docs/users/features/goals.md @@ -0,0 +1,58 @@ +# Goals + +A Goal keeps Qwen Code working across turns until a stated condition is met. Set one with `/goal `; after each turn an independent verifier checks the transcript, and the session keeps going until the objective is verified complete, verified blocked, paused, or cleared. + +## Commands + +| Command | Behavior | +| ------------------------ | ------------------------------------------------------------- | +| `/goal` | Show the current Goal and its status. | +| `/goal ` | Create a Goal, or replace the active one. | +| `/goal set ` | Same as above, explicit form. | +| `/goal edit ` | Revise the active Goal's wording without starting over. | +| `/goal pause` / `resume` | Stop or continue the loop without losing the Goal. | +| `/goal clear` | Remove the Goal. | +| `/goal-draft ` | Have the objective written for you before you set it (below). | + +Creating, editing, or resuming a Goal requires a trusted workspace (`/trust`). Headless usage is covered in [Headless Mode](./headless.md#run-a-persistent-goal). + +## How a Goal is judged + +The verifier never runs commands or reads files on its own. It only sees what is already in the transcript: + +- Visible assistant output and tool results count as evidence. The objective text, your prompts, and the model's hidden reasoning do not. +- Printed text proves only that text was printed. A claim that tests pass, a file changed, or a remote is updated needs the corresponding tool result in the transcript. +- A claim that you confirmed, chose, or approved something needs a real message from you; the verifier rejects proposals that assume it. +- When evidence is missing the verdict is "not yet", not "done". A condition nobody can evidence keeps the loop running until a limit stops it. + +So the objective has to make the agent produce evidence: run the named check and show the decisive output. + +## Writing a good objective + +Put these into the objective, in this order: + +| Part | What to write | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| `Outcome:` | One sentence: what is true when this is done. | +| `Done when:` | Numbered, binary checks. At least one names a command and its expected exit code or output line, and asks for that line to be pasted. | +| `Must not:` | Files not to touch, tests or thresholds not to weaken, irreversible actions (push, delete, publish) not to take. | +| `Budget:` | When to give up: "stop as blocked after 20 turns" or a time limit. | +| `On block:` | What to report when stuck, and which decision a human must make. | +| `Context:` | Only facts the agent cannot find in the workspace: branch, environment, earlier decisions. | + +Keep it to one objective and roughly under 1,200 characters. `/goal set` and `/goal edit` collapse newlines to spaces, so number the items rather than relying on line breaks. + +| Weak | Why it fails | Stronger | +| -------------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| make checkout faster | No threshold, no check. | `Outcome: checkout p95 is below 250 ms. Done when: 1) npm run bench:checkout exits 0 and prints p95 < 250 (paste the line); 2) npm test exits 0. Must not: change the benchmark or skip tests. Budget: stop as blocked after 20 turns.` | +| clean up the auth module | "Clean" has no evidence. | Ask what would be observable: zero lint warnings in `src/auth`, a coverage threshold, a file count. | +| ship the release | Irreversible, and needs a human decision. | Narrow to a checkable pre-release state (tag exists, `npm run release:dry-run` exits 0) and put "do not publish" in `Must not`. | +| after I confirm the design | The verifier cannot see a confirmation that never happened. | Move it to `On block:` as the decision a human must make. | + +## Let `/goal-draft` write it + +`/goal-draft ` 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. + +Pass an existing objective to tighten it: `/goal-draft all tests pass and the lint is clean`. + +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. diff --git a/docs/users/features/headless.md b/docs/users/features/headless.md index a763e5aa1a8..c7c913b3ff1 100644 --- a/docs/users/features/headless.md +++ b/docs/users/features/headless.md @@ -81,6 +81,8 @@ Use the same `qwen --continue -p ""` pattern for the other operations: | `/goal resume` | Resume an eligible Goal and start headless Goal work. | | `/goal clear` | Clear the Goal without confirmation or a model call. | +A Goal is only as good as its completion condition. See [Goals](./goals.md) for what the verifier can and cannot judge, and use `qwen -p "/goal-draft "` to have the objective drafted before you set it. + Runtime-scheduled Goal continuation segments do not count against `--max-session-turns`, but real user prompts still do. Explicit `--max-wall-time` and `--max-tool-calls` budgets continue to apply; exceeding either pauses active Goal work before the run exits with the budget-specific error. With `--output-format stream-json`, each Goal status change emits a `stream_event` whose `event.type` is `goal_state`. This canonical state event is emitted even without `--include-partial-messages`. When partial messages are enabled, the older `active_goal` event follows as a compatibility projection; automation should treat `goal_state` as authoritative. diff --git a/packages/core/src/skills/bundled/goal-draft/SKILL.md b/packages/core/src/skills/bundled/goal-draft/SKILL.md new file mode 100644 index 00000000000..639cf20f7b7 --- /dev/null +++ b/packages/core/src/skills/bundled/goal-draft/SKILL.md @@ -0,0 +1,109 @@ +--- +name: goal-draft +description: Turn a fuzzy intention into a /goal objective the Goal verifier can actually judge - one outcome, numbered binary "Done when" checks that leave evidence in the transcript, guardrails, a budget, and a block protocol. Use when the user wants to set or define a goal, asks whether a goal is good enough, or says "keep going until X". Usage - /goal-draft , or /goal-draft to tighten it. This skill only writes the objective; it never starts the work. +argument-hint: '[intent, or an existing goal to tighten]' +allowedTools: + - get_goal + - read_file + - glob + - grep_search +--- + +# /goal-draft — write a Goal the verifier can judge + +You are already inside the loaded `goal-draft` skill — do not call the `skill` tool to invoke it again; start with Step 0. + +You are drafting the text for `/goal set`. You are NOT doing the work the goal describes. Do not edit files, do not run the checks, do not start on the task. The only deliverable is the objective text and the `/goal set` line the user can run. + +## How Goals are judged (why the format below matters) + +An active Goal is re-fed to the model every turn, and its completion is judged by an independent verifier that sees ONLY transcript evidence: + +- Visible assistant output and tool results count as evidence. The objective itself, user prompts, and hidden reasoning do not. +- `delivered_output` evidence proves only that text was printed. It cannot prove that tests passed, files changed, or remote state changed — those need a tool result in the transcript (an `external_fact`). +- A claim that the user confirmed, chose, or approved something needs a real user message as evidence; otherwise the completion proposal is rejected. +- Vague, subjective, or open-ended conditions never accumulate enough evidence; the loop then runs until a limit is hit. + +So a good objective makes the agent PRODUCE evidence: run the named check and paste the decisive output line. + +## Step 0 — should this be a Goal at all? + +Say no, briefly, when the request is a normal one-shot task, needs a design or product judgement call, or has no way to be checked from the agent's own output. Offer to just do it, or to write a plan instead. A goal that cannot be checked is a prompt, not a goal. + +## Step 1 — check the active Goal + +Call `get_goal`. If a Goal is active, ask whether to edit it (same goal, tighter wording → `/goal edit`) or replace it (`/goal set`). Never draft a second concurrent goal. + +## Step 2 — ground the draft in the workspace + +Before asking anything, verify what you can with `read_file`, `glob`, and `grep_search`: that named files and packages exist, and what the real check commands are (`package.json` scripts, `Makefile`, CI workflow, test config). Use those exact commands in "Done when". Never invent paths, IDs, or commands; write `` for anything you cannot confirm. + +## Step 3 — at most one round of questions + +Ask with `ask_user_question`, 1–3 questions in one call, each with options and a recommended default. Ask only when the answer changes the check, the scope, or the budget. Typical questions: + +- Which check defines success: a test command, a build, a metric threshold, a file or state assertion? +- Which environment: local, CI, staging? +- What is off limits: which files, which actions (push, delete, publish)? +- How long to try before stopping as blocked? + +Rules for the questions: + +- Never ask what you could find out by reading the workspace. +- Batch them into one `ask_user_question` call; do not drip one per turn. +- Only ask things only the user can answer: what counts as done, what is off limits, how long to try. +- If you cannot find a concrete way to verify the outcome, you MUST ask, offering 2–3 candidate checks. Do not skip this and do not invent one. + +If you cannot ask (headless, or a client without prompts), take the recommended default and mark it `[ASSUMPTION]` in Context. + +## Step 4 — draft the objective + +Use exactly these labels, in this order. Keep the whole objective on one line when you hand it over — the `/goal` parser joins lines with spaces, so number items instead of relying on newlines. Body text follows the user's language; labels stay English so the verifier can match them. + +```text +Outcome: +Done when: 1) exits 0 and its output shows <…> (paste that line); 2) ; 3) … +Must not: +Budget: stop as blocked after turns (default 20) or minutes without measurable progress +On block: propose blocked with the exact blocker and the decision a human must make; never claim completion without evidence for every Done-when item +Context: +``` + +Rules of thumb: + +- One Outcome. Several outcomes = several goals, or a checklist file plus a single "every item in `` is checked" goal. +- Every Done-when item is binary, and at least one is tool-observable (a command with an exit code or output line, a file that exists, a grep that matches). +- Prefer "the smallest safe change in ``" over open-ended refactors. +- Put anything that must not change on the way into Must not — this is what stops the loop from deleting a failing test to "pass". +- Keep it short: everything the agent can derive from the workspace stays out. Aim for under ~1200 characters. + +### Weak → strong + +| Weak | Strong | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| make checkout faster | Outcome: checkout API p95 is below 250 ms on the documented slow path. Done when: 1) `npm run bench:checkout` exits 0 and prints a p95 below 250 (paste the line); 2) `npm test` exits 0. Must not: change the benchmark, skip tests, touch files outside `src/checkout`. Budget: stop as blocked after 20 turns. On block: report the measured p95 and what blocks it. | +| keep handling the PR comments | Outcome: every unresolved review thread on PR #123 is fixed or answered. Done when: 1) the review-threads query shows zero unresolved threads (paste the count); 2) CI on the head commit is green (paste the check summary). Must not: force-push, resolve a thread without replying to it. Budget: stop as blocked after 30 turns. On block: list the threads that need a maintainer decision. | +| clean up the auth module | Not a goal — "clean" has no check. Ask what would be observable (zero lint warnings in `src/auth`? a file count? a coverage threshold?) or offer a refactor plan instead. | +| get the release out | Not a goal as written — publishing is irreversible. Either narrow it to a checkable pre-release state (tag exists, changelog entry present, `npm run release:dry-run` exits 0) and put "do not publish" in Must not, or leave publishing to a human. | + +## Step 5 — self-check, then hand off + +Check every line before printing: + +1. "Done when" exists, its items are numbered, and each is binary. +2. At least one item names a command, exit code, file, or grep pattern and asks to paste the output. +3. No subjective adjectives as conditions (clean, better, robust, elegant, reasonable, …). +4. No "after the user confirms/approves" as a completion condition — that belongs in On block as a decision a human must make. +5. Budget or On block is present. +6. Exactly one Outcome. +7. Every path and command in Context was verified in the workspace or is marked ``. +8. Under ~1200 characters. +9. Irreversible actions (push, delete, publish) are listed in Must not, or the user explicitly allowed them. + +Then print, and nothing else: + +1. The objective in a fenced code block. +2. One line the user can run as-is: `/goal set ` (or `/goal edit …` when tightening the active goal). Print it as plain text with no code markers, so it can be copied verbatim. +3. One sentence on what you assumed (`[ASSUMPTION]` / `` items), if any. + +Do not run /goal yourself. Do not begin the task. Stop and wait for the user. diff --git a/packages/core/src/skills/bundled/goal-draft/SKILL.test.ts b/packages/core/src/skills/bundled/goal-draft/SKILL.test.ts new file mode 100644 index 00000000000..2e018c1694d --- /dev/null +++ b/packages/core/src/skills/bundled/goal-draft/SKILL.test.ts @@ -0,0 +1,199 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { + buildPermissionCheckContext, + evaluatePermissionRules, +} from '../../../core/permission-helpers.js'; +import { PermissionManager } from '../../../permissions/permission-manager.js'; +import { applySkillAllowedTools } from '../../../tools/skill-utils.js'; +import { parseSkillContent } from '../../skill-load.js'; + +function loadGoalDraftSkill() { + const skillPath = path.join( + path.dirname(fileURLToPath(import.meta.url)), + 'SKILL.md', + ); + const content = fs.readFileSync(skillPath, 'utf8'); + const config = parseSkillContent(content, skillPath); + return { config, body: config.body }; +} + +describe('bundled goal-draft skill', () => { + it('auto-approves only the non-mutating tools: Goal read and workspace reads', () => { + const { config } = loadGoalDraftSkill(); + + expect(config.name).toBe('goal-draft'); + expect(config.allowedTools).toEqual([ + 'get_goal', + 'read_file', + 'glob', + 'grep_search', + ]); + // allowedTools is an additive auto-approval grant, not a sandbox + // (skills/types.ts) — read-only behavior is enforced by the SKILL.md + // prose. These assertions pin what the grant deliberately excludes: + // drafting must never turn into doing the work or proposing a + // terminal Goal status on the user's behalf. + expect(config.allowedTools).not.toContain('run_shell_command'); + expect(config.allowedTools).not.toContain('write_file'); + expect(config.allowedTools).not.toContain('edit'); + expect(config.allowedTools).not.toContain('update_goal'); + // ask_user_question must stay ungranted: a session-wide allow rule + // overrides its 'ask' default and the scheduler then runs it without + // showing the dialog, fabricating a declined-answer result (see the + // grant test below). + expect(config.allowedTools).not.toContain('ask_user_question'); + }); + + it('keeps ask_user_question behind its dialog after the allowedTools grant', async () => { + const { config } = loadGoalDraftSkill(); + + // BundledSkillLoader applies the frontmatter grant as session-wide + // allow rules. If ask_user_question were granted, that rule would + // override the tool's interactive 'ask' default and it would execute + // with no dialog, returning "User declined to answer" as success. + const pm = new PermissionManager({ + getPermissionsAllow: () => undefined, + getPermissionsAsk: () => undefined, + getPermissionsDeny: () => undefined, + }); + applySkillAllowedTools(pm, config.allowedTools); + + const ctx = buildPermissionCheckContext('ask_user_question', {}, ''); + await expect( + evaluatePermissionRules(pm, 'ask', ctx), + ).resolves.toMatchObject({ finalPermission: 'ask' }); + }); + + it('stays model-invocable and user-invocable so both `/goal-draft` and "define a goal" reach it', () => { + const { config, body } = loadGoalDraftSkill(); + + expect(config.disableModelInvocation).toBeFalsy(); + expect(config.userInvocable ?? true).toBe(true); + expect(config.argumentHint).toBe( + '[intent, or an existing goal to tighten]', + ); + expect(config.description).toContain('/goal-draft'); + expect(config.description).toContain('never starts the work'); + // Both entry paths — the `/goal-draft` slash command and the model's + // own Skill call — inject this body, so a second Skill call can only + // re-request an approval that headless runs cannot give, and the + // session stops without drafting anything. The body must forbid it. + expect(body).toContain('do not call the `skill` tool to invoke it again'); + }); + + it('explains the verifier rules the objective format is derived from', () => { + const { body } = loadGoalDraftSkill(); + + // These mirror goal-verifier.ts / goalJudge.ts: transcript-only + // evidence, delivered_output cannot prove external state, and user + // actions need user_input evidence. + expect(body).toContain('sees ONLY transcript evidence'); + expect(body).toContain('`delivered_output` evidence proves only'); + expect(body).toContain('needs a real user message as evidence'); + expect(body).toContain('paste the decisive output line'); + }); + + it('walks the six steps in order and gates on whether a Goal is warranted', () => { + const { body } = loadGoalDraftSkill(); + + const headings = [ + '## Step 0 — should this be a Goal at all?', + '## Step 1 — check the active Goal', + '## Step 2 — ground the draft in the workspace', + '## Step 3 — at most one round of questions', + '## Step 4 — draft the objective', + '## Step 5 — self-check, then hand off', + ]; + const positions = headings.map((heading) => body.indexOf(heading)); + expect(positions.every((index) => index >= 0)).toBe(true); + expect(positions).toEqual([...positions].sort((a, b) => a - b)); + expect(body).toContain('Call `get_goal`'); + expect(body).toContain('Never draft a second concurrent goal.'); + expect(body).toContain( + 'A goal that cannot be checked is a prompt, not a goal.', + ); + }); + + it('rations clarifying questions and forbids inventing a verification path', () => { + const { body } = loadGoalDraftSkill(); + + expect(body).toContain('1–3 questions in one call'); + expect(body).toContain( + 'Never ask what you could find out by reading the workspace.', + ); + expect(body).toContain('you MUST ask, offering 2–3 candidate checks'); + expect(body).toContain('mark it `[ASSUMPTION]` in Context'); + expect(body).toContain('Never invent paths, IDs, or commands'); + }); + + it('fixes the objective contract labels and keeps the hand-off on one line', () => { + const { body } = loadGoalDraftSkill(); + + // Pin the labels where the drafter copies them from — the ```text + // template — not just anywhere in the body, where the Weak→strong + // table repeats five of the six labels. + const templateStart = body.indexOf('```text'); + const templateEnd = body.indexOf('\n```', templateStart); + expect(templateStart).toBeGreaterThanOrEqual(0); + expect(templateEnd).toBeGreaterThan(templateStart); + const template = body.slice(templateStart, templateEnd); + + let previous = -1; + for (const label of [ + 'Outcome:', + 'Done when:', + 'Must not:', + 'Budget:', + 'On block:', + 'Context:', + ]) { + const position = template.indexOf(label); + expect(position).toBeGreaterThan(previous); + previous = position; + } + // parseGoalCommand joins whitespace-separated tokens with single + // spaces, so a multi-line objective would be flattened anyway. + expect(body).toContain( + 'the `/goal` parser joins lines with spaces, so number items instead of relying on newlines', + ); + expect(body).toContain('`/goal set `'); + expect(body).toContain('or `/goal edit …` when tightening the active goal'); + // Wrapping the hand-off line in inline code makes the terminal render + // escaped backticks, and copying it then pastes backslashes into the + // objective — the line must go out as plain text. + expect(body).toContain('Print it as plain text with no code markers'); + }); + + it('ends with the self-check list and an explicit stop', () => { + const { body } = loadGoalDraftSkill(); + + expect(body).toContain('No subjective adjectives as conditions'); + expect(body).toContain('No "after the user confirms/approves"'); + expect(body).toContain('Exactly one Outcome.'); + expect(body).toContain('Irreversible actions (push, delete, publish)'); + // The stop instruction is pinned as the very last line so the final + // thing the model reads is "do not start". + expect(body.trimEnd().split('\n').pop()).toBe( + 'Do not run /goal yourself. Do not begin the task. Stop and wait for the user.', + ); + // The "do not do the work" instruction is stated up front as well as at + // the end, because skipping straight to implementation is the most + // common failure mode of spec-writing skills. + const upFront = body.indexOf( + 'You are NOT doing the work the goal describes.', + ); + const step0 = body.indexOf('## Step 0'); + expect(upFront).toBeGreaterThanOrEqual(0); + expect(step0).toBeGreaterThanOrEqual(0); + expect(upFront).toBeLessThan(step0); + }); +}); diff --git a/packages/web-shell/client/constants/localCommands.test.ts b/packages/web-shell/client/constants/localCommands.test.ts index 0b8a2d21605..f93c98f19da 100644 --- a/packages/web-shell/client/constants/localCommands.test.ts +++ b/packages/web-shell/client/constants/localCommands.test.ts @@ -116,6 +116,13 @@ describe('skillDescriptionKey', () => { expect(zh(skillDescriptionKey('bugfix')!)).toBe( '按先复现流程修复 GitHub issue 中的 bug', ); + expect(skillDescriptionKey('goal-draft')).toBe('skilldesc.goalDraft'); + expect(en(skillDescriptionKey('goal-draft')!)).toBe( + 'Turn a fuzzy intention into a verifiable /goal objective', + ); + expect(zh(skillDescriptionKey('goal-draft')!)).toBe( + '将模糊意图改写为可验证的 /goal 目标', + ); }); it('returns undefined for an unknown (user) skill', () => { diff --git a/packages/web-shell/client/constants/localCommands.ts b/packages/web-shell/client/constants/localCommands.ts index c572143096b..a0e40409264 100644 --- a/packages/web-shell/client/constants/localCommands.ts +++ b/packages/web-shell/client/constants/localCommands.ts @@ -180,6 +180,7 @@ const SKILL_DESCRIPTION_KEYS: Record = { batch: 'skilldesc.batch', dataviz: 'skilldesc.dataviz', 'extension-creator': 'skilldesc.extensionCreator', + 'goal-draft': 'skilldesc.goalDraft', loop: 'skilldesc.loop', 'new-app': 'skilldesc.newApp', 'qc-helper': 'skilldesc.qcHelper', diff --git a/packages/web-shell/client/i18n.tsx b/packages/web-shell/client/i18n.tsx index 0feeba4f258..6ffab647637 100644 --- a/packages/web-shell/client/i18n.tsx +++ b/packages/web-shell/client/i18n.tsx @@ -1613,6 +1613,8 @@ const EN: Messages = { 'skilldesc.dataviz': 'Design guidance for charts and data visualizations', 'skilldesc.extensionCreator': 'Create, test, and customize Qwen Code extensions', + 'skilldesc.goalDraft': + 'Turn a fuzzy intention into a verifiable /goal objective', 'skilldesc.loop': 'Run a prompt on a schedule or self-paced wakeups', 'skilldesc.newApp': 'Workflow for building a new app from scratch', 'skilldesc.qcHelper': 'Answer questions about using Qwen Code', @@ -2095,7 +2097,8 @@ const EN: Messages = { 'goals.newHint': 'The goal starts in a new session and runs until its condition holds.', 'goals.condition': 'Condition', - 'goals.conditionPlaceholder': 'e.g. all tests pass and the lint is clean', + 'goals.conditionPlaceholder': + 'e.g. `npm test` exits 0 and `npm run lint` reports no warnings (paste the output); do not modify test files; stop as blocked after 20 turns', 'goals.cancel': 'Cancel', 'goals.create': 'Set goal', 'goals.creating': 'Starting…', @@ -4599,6 +4602,7 @@ const ZH: Messages = { 'skilldesc.batch': '并行批量处理多个文件', 'skilldesc.dataviz': '图表与数据可视化设计指南', 'skilldesc.extensionCreator': '创建、测试和定制 Qwen Code 扩展', + 'skilldesc.goalDraft': '将模糊意图改写为可验证的 /goal 目标', 'skilldesc.loop': '按计划或自定节奏循环运行提示词', 'skilldesc.newApp': '从零构建新应用的工作流', 'skilldesc.qcHelper': '解答 Qwen Code 使用相关问题', @@ -5041,7 +5045,8 @@ const ZH: Messages = { 'goals.new': '新建目标', 'goals.newHint': '目标会在一个新会话中启动,并持续运行直到条件满足。', 'goals.condition': '条件', - 'goals.conditionPlaceholder': '例如:所有测试通过且 lint 无告警', + 'goals.conditionPlaceholder': + '例如:`npm test` 退出码为 0 且 `npm run lint` 无告警(贴出输出);不得修改测试文件;20 轮内未达成则以 blocked 结束', 'goals.cancel': '取消', 'goals.create': '设置目标', 'goals.creating': '正在启动…',