feat(core): structured system prompt v1 (composer + 8 sections)#22
feat(core): structured system prompt v1 (composer + 8 sections)#22
Conversation
…ser) - Add packages/core/src/prompts/ with 7 independently-authored .txt section files (identity, workflow, output-rules, design-methodology, tweaks-protocol, anti-slop, safety) and an index.ts composer that assembles them per mode - Composer is mode-aware: create / tweak / revise produce different prompts; tweak additionally injects the EDITMODE protocol section - Brand tokens from StoredDesignSystem are serialized and appended when provided - Wire generate() and applyComment() in core/src/index.ts to use composeSystemPrompt() instead of the single SYSTEM_PROMPTS.designGenerator string; systemPrompt override still supported for backward compat - Add 3 unit tests for composeSystemPrompt (create / tweak / brandTokens) - Add docs/research/11-custom-sliders.md (new, with⚠️ correction header): replaces the inferred <script type="application/json"> mechanism with the correct EDITMODE marker-block + postMessage(__edit_mode_set_keys) protocol - Add docs/research/15-claude-design-prompts.md: structural analysis of the leaked Claude Design prompt; no verbatim text reproduced (legal note included) Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Blocker] EDITMODE listener is attached to
documentinstead ofwindow—postMessageevents are dispatched onwindow, so generated tweakable artifacts may ignore slider/token updates entirely, breaking the EDITMODE interaction contract. Evidence:packages/core/src/prompts/index.ts:183, mirrored inpackages/core/src/prompts/tweaks-protocol.v1.txt:27.
Suggested fix:// inside the EDITMODE script example window.addEventListener('message', handleEdits); function handleEdits(e) { if (!e.data || e.data.type !== '__edit_mode_set_keys') return; const root = document.documentElement; for (const [key, value] of Object.entries(e.data.edits)) { root.style.setProperty('--' + key, String(value)); } }
Summary
- Review mode: initial
- 1 blocker found in the newly introduced tweaks protocol instructions. If uncorrected, model-generated artifacts can fail to react to runtime parameter updates.
Testing
- Not run (automation)
- Suggested: add a Vitest assertion for tweak mode prompt content to require
window.addEventListener('message'and rejectdocument.addEventListener('message'.
open-codesign Bot
|
|
||
| // The script may also contain runtime logic below the EDITMODE block. | ||
| // The block itself is a pure JSON object literal — no trailing commas. | ||
| document.addEventListener('message', handleEdits); |
There was a problem hiding this comment.
postMessage events are delivered on window, not document; this instruction can cause generated artifacts to miss __edit_mode_set_keys updates. Please switch to window.addEventListener('message', handleEdits) here (and mirror the same fix in tweaks-protocol.v1.txt).
document.addEventListener does not receive window.postMessage events; the listener must be on window. Fixes slider/token-editor edits being silently swallowed in the sandbox iframe. Also adds String(value) cast for safety. Test added to prevent regression. Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
-
[Major]
generate()exposes unsupportedtweakmode contract —GenerateInput.modeacceptstweak, butgenerate()still sends a standard prompt (buildPrompt) and does not provide the current artifact HTML + changed params that the tweak protocol explicitly requires. This can produce invalid/low-quality outputs if callers setmode: 'tweak'. Evidence:packages/core/src/index.ts:46,packages/core/src/index.ts:278,packages/core/src/prompts/index.ts:222.
Suggested fix:// packages/core/src/index.ts export interface GenerateInput { // ... mode?: Extract<PromptComposeOptions['mode'], 'create'> | undefined; } // alternatively, guard explicitly if (input.mode === 'tweak') { throw new CodesignError( 'Tweak mode requires current artifact + edits context; use a dedicated tweak API', 'INPUT_UNSUPPORTED_MODE', ); }
-
[Minor] Prompt source-of-truth drift between
.txtand inlined constant — the composer header says.txtand TS constants must stay in sync, but the new tweaks section differs (String(value)in TS vs rawvaluein.txt). This increases maintenance risk and can cause reviewers to approve the wrong instruction text. Evidence:packages/core/src/prompts/index.ts:9,packages/core/src/prompts/index.ts:189,packages/core/src/prompts/tweaks-protocol.v1.txt:33.
Suggested fix:// packages/core/src/prompts/tweaks-protocol.v1.txt root.style.setProperty('--' + key, String(value));
Summary
- Review mode: follow-up after new commits
- 2 issues found in the current head diff. The previous blocker (
window.addEventListener('message')) is fixed, but the API-mode mismatch and prompt-source drift remain.
Testing
- Not run (automation)
- Suggested: add a test that
generate()rejects/guardsmode: 'tweak'until tweak-specific input shape is implemented.
open-codesign Bot
| /** Override the system prompt entirely. When set, `mode` is ignored. */ | ||
| systemPrompt?: string | undefined; | ||
| /** Generation mode. Defaults to 'create'. */ | ||
| mode?: PromptComposeOptions['mode'] | undefined; |
There was a problem hiding this comment.
mode currently allows tweak, but generate() still sends the regular user prompt shape. That conflicts with the tweak protocol contract and can produce incorrect generations if callers pass mode: "tweak". Please either narrow this type to create for now or hard-throw on tweak until a dedicated tweak input API exists.
| if (!e.data || e.data.type !== '__edit_mode_set_keys') return; | ||
| const root = document.documentElement; | ||
| for (const [key, value] of Object.entries(e.data.edits)) { | ||
| root.style.setProperty('--' + key, value); |
There was a problem hiding this comment.
This .txt instruction now diverges from the inlined TS prompt constant (String(value) vs raw value). Since this file is documented as source-coupled with index.ts, please keep them identical to avoid future edits landing in one source but not the runtime prompt.
…ith TS
Closes the contract leak where callers could pass mode:'tweak' or 'revise'
to generate() and silently receive wrong output.
- Narrow GenerateInput.mode to Extract<PromptComposeOptions['mode'],'create'>
so TypeScript rejects unsupported values at compile time.
- Add a runtime guard that throws CodesignError('INPUT_UNSUPPORTED_MODE')
for any non-'create' mode value, in case callers bypass the type.
- Hard-wire composeSystemPrompt({mode:'create'}) inside generate() instead
of forwarding input.mode, ensuring tweak/revise prompt sections never leak.
- Sync tweaks-protocol.v1.txt: replace bare `value` with `String(value)` in
the setProperty call to match the inlined TS constant (future: build-time
injection from .txt to eliminate this drift entirely).
- Add 3 tests: INPUT_UNSUPPORTED_MODE guard, mode:'create' happy path, and
.txt vs TS constant parity for the setProperty line.
Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Minor]
systemPromptoverride contract is contradicted by the new mode guard — the API docs statesystemPromptmakesmodeignored, butgenerate()now throwsINPUT_UNSUPPORTED_MODEbefore usingsystemPromptwhen a non-createmode is present at runtime. This can break callers that pass through metadata mode values while intentionally overriding the full system prompt. Evidence:packages/core/src/index.ts:43,packages/core/src/index.ts:277.
Suggested fix:// Only enforce mode when composing the built-in prompt if (!input.systemPrompt && input.mode && input.mode !== 'create') { throw new CodesignError( 'generate() only supports mode "create". Use applyComment() for revise; tweak is not yet wired.', 'INPUT_UNSUPPORTED_MODE', ); }
Summary
- Review mode: follow-up after new commits
- 1 issue found in the latest head diff.
Testing
- Not run (automation:
pnpmis unavailable in this runner)
open-codesign Bot
|
|
||
| // Narrow guard: only 'create' is wired through buildPrompt. Callers passing | ||
| // 'tweak' or 'revise' would silently get wrong output — reject early instead. | ||
| if (input.mode && input.mode !== 'create') { |
There was a problem hiding this comment.
This guard now runs even when systemPrompt is provided, which conflicts with the documented contract on line 43 (mode ignored when system prompt is overridden). Consider gating this check with !input.systemPrompt so custom prompt callers do not fail on pass-through mode metadata.
The built-in mode guard was firing even when callers passed a custom systemPrompt, which the contract says causes mode to be ignored. Guard now checks `!input.systemPrompt` first, resolving the contradiction. Added a test confirming that mode: 'tweak' + systemPrompt does NOT throw, while mode: 'tweak' without systemPrompt still does. Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Minor] Unused
@open-codesign/templatesdependency remains after migrating prompt generation tocomposeSystemPrompt, which increases dependency budget and install surface without runtime value. Evidencepackages/core/src/index.ts:11(import removed from templates and replaced with local prompts), with remaining dependency inpackages/core/package.json:17.
Suggested fix:{ "dependencies": { "@open-codesign/artifacts": "workspace:*", "@open-codesign/providers": "workspace:*", "@open-codesign/shared": "workspace:*" } }
Summary
- Review mode: follow-up after new commits
- 1 issue found in the latest head diff.
Testing
- Not run (automation:
pnpmis unavailable in this runner)
open-codesign Bot
| type SerializedBrandTokens, | ||
| composeSystemPrompt, | ||
| storedDesignSystemToTokens, | ||
| } from './prompts/index.js'; |
There was a problem hiding this comment.
@open-codesign/templates appears to be orphaned after this file switched to local composeSystemPrompt imports. Keeping it adds unnecessary dependency surface against the ≤30 prod-deps / no-bloat constraints.
Suggested fix:
{
"dependencies": {
"@open-codesign/artifacts": "workspace:*",
"@open-codesign/providers": "workspace:*",
"@open-codesign/shared": "workspace:*"
}
}Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Major] Prompt section drift is not actually enforced end-to-end — runtime prompt text is duplicated in TS constants while sibling
.txtfiles are intended as the editable source, but the added drift test only checks one substring in one section. This can silently desync reviewed prompt docs from shipped runtime behavior and weaken safety/quality constraints over time, evidencepackages/core/src/prompts/index.ts:4,packages/core/src/generate.test.ts:352.
Suggested fix:// packages/core/src/prompts/index.ts export const PROMPT_SECTIONS = { identity: IDENTITY, workflow: WORKFLOW, outputRules: OUTPUT_RULES, designMethodology: DESIGN_METHODOLOGY, tweaksProtocol: TWEAKS_PROTOCOL, antiSlop: ANTI_SLOP, safety: SAFETY, } as const;
// packages/core/src/generate.test.ts const expected = readFileSync(path.join(__dirname, 'prompts', 'workflow.v1.txt'), 'utf-8').trim(); expect(PROMPT_SECTIONS.workflow.trim()).toBe(expected); // Repeat for all section files, not only tweaks-protocol.
Summary
- Review mode: follow-up after new commits
- 1 major issue found in this head. Follow-up commit correctly removed the unnecessary
@open-codesign/templatesdependency, but prompt source-of-truth drift protection is still incomplete.
Testing
- Not run (automation):
pnpmis unavailable in this runner (pnpm: command not found).
open-codesign Bot
| }); | ||
|
|
||
| describe('tweaks-protocol .txt vs TS constant drift', () => { | ||
| it('EDITMODE block in .txt and the inlined TS constant are character-for-character identical', () => { |
There was a problem hiding this comment.
The test title says "character-for-character identical", but this assertion only checks one line and one prompt mode. Please enforce full .txt ↔ TS equality for every prompt section to prevent silent runtime drift.
…ions Add PROMPT_SECTIONS and PROMPT_SECTION_FILES exports to prompts/index.ts, and replace the single-point tweaks-protocol drift test with a loop that covers all 7 sections (identity, workflow, outputRules, designMethodology, tweaksProtocol, antiSlop, safety) byte-for-byte. Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Blocker] Untrusted design-system content is promoted into the system prompt, enabling prompt-injection from scanned repositories — this elevates untrusted text (e.g., token names/summary extracted from code) to highest-priority instructions and can override safety/format constraints. Evidence
packages/core/src/index.ts:291,packages/core/src/index.ts:323,packages/core/src/prompts/index.ts:378.
Suggested fix:// Keep system prompt static/trusted; pass project-derived tokens only in user/context messages. content: input.systemPrompt ?? composeSystemPrompt({ mode: 'create' }); // ... content: composeSystemPrompt({ mode: 'revise' });
Summary
- Review mode: follow-up after new commits
- 1 blocker found. Other hard-constraint violations were not found in modified lines.
Testing
- Not run (automation):
pnpmis unavailable in this runner (pnpm: command not found).
open-codesign Bot
| input.systemPrompt ?? | ||
| composeSystemPrompt({ | ||
| mode: 'create', | ||
| brandTokens: input.designSystem ? storedDesignSystemToTokens(input.designSystem) : null, |
There was a problem hiding this comment.
Untrusted values from scanned repositories are injected into the system-role prompt here (brandTokens), which creates a prompt-injection path. Keep system prompts static and pass repository-derived data in the user/context message only.
…age with untrusted tag Brand tokens and other codebase-scanned content were previously serialized directly into the system prompt via composeSystemPrompt(). Adversarial text embedded in a user's design system (e.g. "Ignore previous instructions") could reach the highest-priority role and bypass anti-slop / safety constraints. Changes: - Remove `brandTokens` from `PromptComposeOptions` and `SerializedBrandTokens`/ `storedDesignSystemToTokens` from `prompts/index.ts` entirely; the system prompt is now fully static trusted content. - Wrap `formatDesignSystem()` output in `<untrusted_scanned_content type="design_system">` so the model receives brand tokens as data-only user content, not system instructions. - Add "Untrusted scanned content" section to SAFETY (both TS constant and safety.v1.txt) instructing the model to treat the tag's contents as data. - Remove unused `SerializedBrandTokens` / `storedDesignSystemToTokens` exports from `core/src/index.ts`. - Add 3 new tests: (1) brand tokens in user message not system, (2) adversarial text only in user message, (3) composeSystemPrompt never includes token values. Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- [Major] Untrusted-content wrapper can be broken by injected closing tag —
formatDesignSystem()interpolates repository-derived strings directly inside<untrusted_scanned_content>...</untrusted_scanned_content>. If scanned content contains</untrusted_scanned_content>, attacker-controlled text escapes the protected region and can appear as normal user instructions, weakening the prompt-injection mitigation. Evidencepackages/core/src/index.ts:159.
Suggested fix:function escapeUntrustedXml(text: string): string { return text .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>'); } const payload = escapeUntrustedXml(lines.join('\n')); return `<untrusted_scanned_content type="design_system">\n${payload}\n</untrusted_scanned_content>`;
Summary
- Review mode: follow-up after new commits
- 1 major issue found.
- Required context files
docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs.
Testing
- Not run (automation).
open-codesign Bot
| return lines.join('\n'); | ||
| // Wrap in untrusted tag — codebase content may contain adversarial text. | ||
| // The system prompt instructs the model to treat this as data only. | ||
| return `<untrusted_scanned_content type="design_system"> |
There was a problem hiding this comment.
designSystem content is untrusted, but this interpolates it directly into XML-like tags. A scanned value containing </untrusted_scanned_content> can break out of the wrapper and bypass your containment convention. Please escape <, >, and & (or serialize as JSON/code block) before interpolation.
Malicious scanned content containing </untrusted_scanned_content> could escape the wrapper tag and inject arbitrary XML into the prompt. Add escapeUntrustedXml() and apply it in formatDesignSystem() before the payload is interpolated. Covers &, <, > to block all tag-break vectors. Add a Vitest test verifying that a closing-tag injection is rendered as </untrusted_scanned_content> in the outgoing user message. Signed-off-by: hqhq1025 <1506751656@qq.com>
There was a problem hiding this comment.
Findings
- No diff-attributable issues found on added/modified lines.
Summary
- Review mode: follow-up after new commits
- No blocking/major/minor/nit issues identified in the current head diff.
- Required context files
docs/VISION.mdanddocs/PRINCIPLES.md: Not found in repo/docs. - Residual risk: runtime verification is limited in this environment because package tooling is unavailable.
Testing
- Not run (automation):
pnpmis not available in the runner.
open-codesign Bot
Summary
identity,workflow,output-rules,design-methodology,tweaks-protocol,anti-slop,safety) underpackages/core/src/prompts/, plus anindex.tscomposer.txtfile (human-readable in PR diffs) with a matching TS string constant inlined inindex.ts— no runtimefsdependency, Vite-compatiblecreate/tweak/reviseproduce different prompts;tweakadditionally injects the EDITMODE protocol sectionStoredDesignSystemare serialized and appended when providedgenerate()andapplyComment()incore/src/index.tsnow callcomposeSystemPrompt()instead of the singleSYSTEM_PROMPTS.designGeneratorstring; thesystemPromptoverride field is preserved for backward compatLegal / License
Research corrections
docs/research/11-custom-sliders.md: documents the correct EDITMODE mechanism (/*EDITMODE-BEGIN*/{...}/*EDITMODE-END*/+postMessage(__edit_mode_set_keys)) with a prominent correction header explaining the earlier<script type="application/json">inference was wrongdocs/research/15-claude-design-prompts.md: structural analysis of the leaked prompt (no verbatim reproduction)Compatibility ✅ Upgradeability ✅ No bloat ✅ Elegance ✅
systemPromptpassthrough preserved — callers that set it explicitly are unaffectedTest plan
composeSystemPrompt({ mode: 'create' })contains identity + workflow + anti-slopcomposeSystemPrompt({ mode: 'tweak' })additionally contains EDITMODE /__edit_mode_set_keyscomposeSystemPrompt({ mode: 'create', brandTokens: {...} })serializes brand tokensgenerate()default system prompt contains 'open-codesign' and 'artifact'applyComment()system prompt contains 'Revision workflow'@open-codesign/core; lint 0 errors