Fix Simple Deck discovery form contract (#2602)

This commit is contained in:
Yuhao Chen
2026-05-22 14:05:24 +08:00
committed by GitHub
parent 63c8dd1fed
commit bddec2391d
3 changed files with 82 additions and 8 deletions
@@ -0,0 +1,43 @@
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const discoveryAtomPath = fileURLToPath(
new URL('../../../plugins/_official/atoms/discovery-question-form/SKILL.md', import.meta.url),
);
const simpleDeckManifestPath = fileURLToPath(
new URL('../../../plugins/_official/examples/simple-deck/open-design.json', import.meta.url),
);
describe('bundled discovery-question-form atom prompt contract', () => {
it('is included in Simple Deck before generation starts', async () => {
const manifest = JSON.parse(await readFile(simpleDeckManifestPath, 'utf8')) as {
od?: { pipeline?: { stages?: Array<{ id?: string; atoms?: string[] }> } };
};
const stages = manifest.od?.pipeline?.stages ?? [];
expect(stages.map((stage) => stage.id)).toEqual(['discovery', 'generate']);
expect(stages[0]?.atoms).toContain('discovery-question-form');
expect(stages[1]?.atoms).toEqual(['file-write', 'live-artifact']);
});
it('teaches agents to emit the wrapped question-form renderer contract', async () => {
const body = await readFile(discoveryAtomPath, 'utf8');
expect(body).toContain('<question-form id="discovery"');
expect(body).toContain('"questions": [');
expect(body).toContain('</question-form>');
expect(body).toMatch(/Do not emit a bare question object by itself/);
});
it('does not present a bare keyed question JSON object as the canonical emission shape', async () => {
const body = await readFile(discoveryAtomPath, 'utf8');
const emissionShape = body.slice(
body.indexOf('## Emission shape'),
body.indexOf('## Question object shape'),
);
expect(emissionShape).not.toMatch(/```jsonc\s*\{\s*"id":/s);
expect(body).not.toMatch(/```jsonc[\s\S]*"id": "audience"/);
});
});
@@ -20,19 +20,44 @@ up turn doesn't re-ask).
- Brief explicitly invites questions ("ask me anything if unclear").
- The discovery skill or pipeline declares a `discovery` stage.
## Question shape (as the agent emits one)
## Emission shape
```jsonc
Emit the form as a `question-form` block whose body is a JSON object with a
top-level `questions` array. Do not emit a bare question object by itself; the
renderer only recognizes the wrapped form contract.
```html
<question-form id="discovery" title="Quick brief — 30 seconds">
{
"id": "audience",
"label": "Who's the primary audience?",
"type": "checkbox", // or 'radio' | 'text' | 'number'
"options": ["VC", "Customer", "Internal team"],
"maxSelections": 2,
"required": true
"description": "I'll lock these in before building. Skip what doesn't apply — I'll fill defaults.",
"questions": [
{
"id": "audience",
"label": "Who's the primary audience?",
"type": "checkbox",
"options": ["VC", "Customer", "Internal team"],
"maxSelections": 2,
"required": true
}
]
}
</question-form>
```
## Question object shape
Each entry in the top-level `questions` array uses:
- `id`: stable answer key, for example `audience`.
- `label`: user-facing question copy.
- `type`: one of `radio`, `checkbox`, `select`, `text`, or `textarea`.
- `options`: required for choice controls; strings are allowed, or objects with
localized `label` and stable `value`.
- `maxSelections`: include this for checkbox controls with a limited selection
count.
- `required`: set to `true` only when the answer is needed before work can
continue.
## Convergence
The discovery atom completes when every required question has an answer
@@ -125,6 +125,12 @@
},
"pipeline": {
"stages": [
{
"id": "discovery",
"atoms": [
"discovery-question-form"
]
},
{
"id": "generate",
"atoms": [