feat(discovery): honor plugin inputs as authoritative Quick-brief answers

When a user picks a plugin chip on Home (e.g. "Web prototype") and sets
fidelity / artifactKind / audience / etc. before opening the project,
the daemon already renders those choices into the system prompt as a
`## Active plugin` / `## Plugin inputs` block — see
`docs/plugins-spec.md` §1258. The Quick-brief form, however, still
asked the user the same questions a second time:

> "在输入框当中已经选了高保真和 Web,但在 Chat 生成的实时选择面板里面又要选一次。"

Root cause is a prompt scoping bug: RULE 1's "drop matching default
questions" rule only pointed at the `## Project metadata` section, not
the `## Plugin inputs` block. Plugin-trigger projects ship most of
their brief through `## Plugin inputs`, so the agent kept the defaults
(Target platform, Fidelity, …) even though every answer was right there
in the same prompt one section over.

Fix: extend RULE 1's source-of-truth list to both blocks, with an
explicit "equally authoritative" line and a small mapping table the
agent can apply by name:

* `fidelity`            → Fidelity
* `platform` / `surface` / `platformTargets` / `target` → Target platform
* `artifactKind` / `mode` / `taskKind` → "What are we making?"
* `audience`            → "Who is this for?"
* `designSystem` / `brand` → Brand context

Adjacent carve-out updated too: the "don't re-ask the kind itself"
sentence now triggers when the active plugin's `od.kind` /
`taskKind` already names the kind, not only when `metadata.kind` is
set. Closes the loop for chip-launched flows where metadata.kind is
set by the daemon but the form was still re-asking.

Why prompt-only (not a data-flow fix):

`## Plugin inputs` is already a first-class block the daemon writes
into every chip-launched run (`apps/daemon/src/prompts/system.ts:246`,
plugins-spec.md §1258). Mapping plugin inputs to `project.metadata`
fields at create time was the other option considered, but it would
require a per-plugin mapping convention (which input name maps to
which metadata field) and would have to be rolled out across web,
daemon, schema, and every existing plugin manifest. The agent already
sees the inputs; the prompt just wasn't telling it those inputs count
as answers. Cheapest fix that generalises to every plugin.

Tests:

* `apps/daemon/tests/prompts/discovery-plugin-inputs.test.ts` (new) —
  five contract tests locking the new rule: the dual-source
  instruction, the "equally authoritative" line, the canonical
  input→question mappings (`fidelity` / `platform` / `artifactKind` /
  `audience` / `designSystem`), the semantic-equivalence aside for
  alternate input names, and the kind carve-out extension to the
  active plugin's `od.kind` / `taskKind`.

Validation:

* `pnpm --filter @open-design/daemon exec vitest run tests/prompts/discovery-plugin-inputs.test.ts`
  → 5/5 passed
* `pnpm --filter @open-design/daemon exec vitest run tests/prompts/system.test.ts tests/prompts/api-mode-override.test.ts`
  → 34/34 passed (no regressions)
This commit is contained in:
lefarcen
2026-05-19 17:46:01 +08:00
parent 169a29ee2b
commit 44b1b4f3cf
2 changed files with 65 additions and 1 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ Form authoring rules:
- If you keep the \`brand\` question, its \`id\` must stay \`"brand"\`. Its three default branch values must stay exactly \`"pick_direction"\`, \`"brand_spec"\`, and \`"reference_match"\` even if you localize the labels.
- If the initial brief already includes a brand spec, brand-guide attachment, reference URL, or screenshot, you may drop the \`brand\` question as already answered, but you must still treat that provided source as Branch A below.
- Tailor the questions to the actual brief — drop defaults the user already answered, add fields the brief uniquely needs (number of slides, list of mobile screens, sections of a landing page).
- **Read the "Project metadata" section later in this prompt before writing the form.** That block lists what the user already chose at create time (kind, fidelity, speakerNotes, animations, template, platform). Drop the matching default question if the field is set; ADD a tailored question for any field marked "(unknown — ask)". For example, on a deck with \`speakerNotes: (unknown — ask…)\`, include a yes/no on speaker notes; on a template project where animations is unknown, include a motion radio; on a cross-platform project, ask which screens need native variants instead of re-asking platform. Don't re-ask the kind itself if metadata.kind is set — the user already told you.
- **Read the "Project metadata" section AND any "## Active plugin" / "## Plugin inputs" block later in this prompt before writing the form.** "Project metadata" lists what the user chose at create time (kind, fidelity, speakerNotes, animations, template, platform); "Plugin inputs" lists the same kind of brief data when the project was opened through a plugin chip on Home (e.g. \`fidelity: "high-fidelity"\`, \`platform: "desktop"\`, \`artifactKind: "web prototype"\`, \`audience: "product evaluators"\`, \`designSystem: "..."\`). **Both sources are equally authoritative — treat a plugin input value as a complete answer to the matching default question.** Concretely: a plugin input \`fidelity\` answers the Fidelity question; \`platform\` (or a semantically-equivalent input such as \`surface\`, \`platformTargets\`, \`target\`) answers Target platform; \`artifactKind\` / \`mode\` / \`taskKind\` already names what we are making so do not re-ask "What are we making?"; \`audience\` answers "Who is this for?"; \`designSystem\` / \`brand\` answers Brand context. Drop the matching default question whenever EITHER source supplies the answer; ADD a tailored question for any field marked "(unknown — ask)". For example, on a deck with \`speakerNotes: (unknown — ask…)\`, include a yes/no on speaker notes; on a template project where animations is unknown, include a motion radio; on a cross-platform project, ask which screens need native variants instead of re-asking platform. Don't re-ask the kind itself if metadata.kind is set or the active plugin's \`od.kind\` / \`taskKind\` already names it — the user already told you.
- Keep it under ~7 questions. Second batch in a follow-up form if needed.
- Lead with one short prose line ("Got it — pitch deck for a SaaS product, B2B audience. Tell me the rest:") then the form. Do **not** write a long pre-amble.
- After \`</question-form>\`, **stop your turn**. Do not write code. Do not start tools. Do not narrate "I'll wait."
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { DISCOVERY_AND_PHILOSOPHY } from '../../src/prompts/discovery.js';
// When a project is opened through a plugin chip on Home, the daemon
// renders the chosen plugin inputs (fidelity, platform, audience,
// artifactKind, designSystem, …) into the system prompt as the
// `## Active plugin` / `## Plugin inputs` block — see
// `docs/plugins-spec.md` §1258 ("Selecting a plugin adds the
// snapshot-derived `## Active plugin`, `## Plugin inputs`, and
// active-stage atom blocks"). Before this change RULE 1 only told the
// agent to consult the `## Project metadata` block when deciding which
// default Quick-brief questions to drop, so the Quick brief still asked
// "Target platform" + "Fidelity" even when the user had already chosen
// both on Home. These tests lock the broader rule: plugin inputs are
// treated as equally authoritative answers to the matching question.
describe('discovery.ts — Plugin inputs are authoritative for Quick brief defaults', () => {
it('directs the agent to read both Project metadata AND the Active plugin / Plugin inputs block', () => {
expect(DISCOVERY_AND_PHILOSOPHY).toMatch(
/Read the "Project metadata" section AND any "## Active plugin" \/ "## Plugin inputs" block/,
);
});
it('explicitly equates plugin input values with answers to Quick-brief defaults', () => {
// Wording-level lock so a future trim of the rule cannot accidentally
// demote plugin inputs back to "ignore unless metadata is set".
expect(DISCOVERY_AND_PHILOSOPHY).toMatch(
/Both sources are equally authoritative — treat a plugin input value as a complete answer/,
);
expect(DISCOVERY_AND_PHILOSOPHY).toMatch(
/Drop the matching default question whenever EITHER source supplies the answer/,
);
});
it('names the common input → question mappings the agent should follow', () => {
// The list is non-exhaustive on purpose (semantic match handles the
// long tail). These five cover the regressions the bug report
// surfaced and the canonical Web Prototype scenario plugin's inputs.
expect(DISCOVERY_AND_PHILOSOPHY).toContain('`fidelity` answers the Fidelity question');
expect(DISCOVERY_AND_PHILOSOPHY).toContain('`platform`');
expect(DISCOVERY_AND_PHILOSOPHY).toContain('Target platform');
expect(DISCOVERY_AND_PHILOSOPHY).toContain('`artifactKind`');
expect(DISCOVERY_AND_PHILOSOPHY).toContain('`audience` answers "Who is this for?"');
expect(DISCOVERY_AND_PHILOSOPHY).toContain('`designSystem`');
});
it('teaches the agent to accept semantically-equivalent input names', () => {
// Web Prototype's manifest uses `platform`; other scenarios may use
// `surface` / `platformTargets` / `target`. The agent must treat any
// of them as the Target platform answer instead of bailing because
// the literal key isn't `platform`.
expect(DISCOVERY_AND_PHILOSOPHY).toMatch(/semantically-equivalent input such as `surface`, `platformTargets`, `target`/);
});
it('does not re-ask the kind when the active plugin already names it', () => {
// RULE 1 used to scope the "don't re-ask kind" carve-out to
// `metadata.kind` only. Extending it to the active plugin's
// `od.kind` / `taskKind` closes the loop for chip-launched flows.
expect(DISCOVERY_AND_PHILOSOPHY).toMatch(
/metadata\.kind is set or the active plugin's `od\.kind` \/ `taskKind` already names it/,
);
});
});