Skip to content

feat(core): structured system prompt v1 (composer + 8 sections)#22

Merged
hqhq1025 merged 8 commits intomainfrom
wt/prompts-v1
Apr 18, 2026
Merged

feat(core): structured system prompt v1 (composer + 8 sections)#22
hqhq1025 merged 8 commits intomainfrom
wt/prompts-v1

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Summary

  • Splits the system prompt into 7 independently-authored section files (identity, workflow, output-rules, design-methodology, tweaks-protocol, anti-slop, safety) under packages/core/src/prompts/, plus an index.ts composer
  • Each section is a .txt file (human-readable in PR diffs) with a matching TS string constant inlined in index.ts — no runtime fs dependency, Vite-compatible
  • 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
  • generate() and applyComment() in core/src/index.ts now call composeSystemPrompt() instead of the single SYSTEM_PROMPTS.designGenerator string; the systemPrompt override field is preserved for backward compat

Legal / License

  • All prompt text is independently authored; no verbatim text from the leaked Claude Design prompt is included
  • Structure and design patterns are our own interpretation
  • Apache-2.0 compatible

Research corrections

  • New 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 wrong
  • New docs/research/15-claude-design-prompts.md: structural analysis of the leaked prompt (no verbatim reproduction)

Compatibility ✅ Upgradeability ✅ No bloat ✅ Elegance ✅

  • No new dependencies
  • systemPrompt passthrough preserved — callers that set it explicitly are unaffected
  • Sections can be updated independently without touching the composer

Test plan

  • composeSystemPrompt({ mode: 'create' }) contains identity + workflow + anti-slop
  • composeSystemPrompt({ mode: 'tweak' }) additionally contains EDITMODE / __edit_mode_set_keys
  • composeSystemPrompt({ mode: 'create', brandTokens: {...} }) serializes brand tokens
  • generate() default system prompt contains 'open-codesign' and 'artifact'
  • applyComment() system prompt contains 'Revision workflow'
  • All 11 tests pass; typecheck clean on @open-codesign/core; lint 0 errors

…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>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Blocker] EDITMODE listener is attached to document instead of windowpostMessage events are dispatched on window, so generated tweakable artifacts may ignore slider/token updates entirely, breaking the EDITMODE interaction contract. Evidence: packages/core/src/prompts/index.ts:183, mirrored in packages/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 reject document.addEventListener('message'.

open-codesign Bot

Comment thread packages/core/src/prompts/index.ts Outdated

// 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] generate() exposes unsupported tweak mode contract — GenerateInput.mode accepts tweak, but generate() 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 set mode: '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 .txt and inlined constant — the composer header says .txt and TS constants must stay in sync, but the new tweaks section differs (String(value) in TS vs raw value in .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/guards mode: 'tweak' until tweak-specific input shape is implemented.

open-codesign Bot

Comment thread packages/core/src/index.ts Outdated
/** Override the system prompt entirely. When set, `mode` is ignored. */
systemPrompt?: string | undefined;
/** Generation mode. Defaults to 'create'. */
mode?: PromptComposeOptions['mode'] | undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] systemPrompt override contract is contradicted by the new mode guard — the API docs state systemPrompt makes mode ignored, but generate() now throws INPUT_UNSUPPORTED_MODE before using systemPrompt when a non-create mode 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: pnpm is unavailable in this runner)

open-codesign Bot

Comment thread packages/core/src/index.ts Outdated

// 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') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] Unused @open-codesign/templates dependency remains after migrating prompt generation to composeSystemPrompt, which increases dependency budget and install surface without runtime value. Evidence packages/core/src/index.ts:11 (import removed from templates and replaced with local prompts), with remaining dependency in packages/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: pnpm is unavailable in this runner)

open-codesign Bot

Comment thread packages/core/src/index.ts Outdated
type SerializedBrandTokens,
composeSystemPrompt,
storedDesignSystemToTokens,
} from './prompts/index.js';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Prompt section drift is not actually enforced end-to-end — runtime prompt text is duplicated in TS constants while sibling .txt files 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, evidence packages/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/templates dependency, but prompt source-of-truth drift protection is still incomplete.

Testing

  • Not run (automation): pnpm is unavailable in this runner (pnpm: command not found).

open-codesign Bot

Comment thread packages/core/src/generate.test.ts Outdated
});

describe('tweaks-protocol .txt vs TS constant drift', () => {
it('EDITMODE block in .txt and the inlined TS constant are character-for-character identical', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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): pnpm is unavailable in this runner (pnpm: command not found).

open-codesign Bot

Comment thread packages/core/src/index.ts Outdated
input.systemPrompt ??
composeSystemPrompt({
mode: 'create',
brandTokens: input.designSystem ? storedDesignSystemToTokens(input.designSystem) : null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. Evidence packages/core/src/index.ts:159.
    Suggested fix:
    function escapeUntrustedXml(text: string): string {
      return text
        .replaceAll('&', '&amp;')
        .replaceAll('<', '&lt;')
        .replaceAll('>', '&gt;');
    }
    
    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.md and docs/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">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
&lt;/untrusted_scanned_content&gt; in the outgoing user message.

Signed-off-by: hqhq1025 <1506751656@qq.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.md and docs/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): pnpm is not available in the runner.

open-codesign Bot

@hqhq1025 hqhq1025 merged commit 7dfc400 into main Apr 18, 2026
6 checks passed
@hqhq1025 hqhq1025 deleted the wt/prompts-v1 branch April 18, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant