This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/gsd-openclaw/.planning/phases/02-integration-layer/02-RESEARCH.md
T

23 KiB

Phase 2: Integration Layer - Research

Researched: 2026-03-11 Domain: OpenClaw skill integration, Feishu Card Message API, response formatting Confidence: HIGH

Summary

Phase 2 establishes the "face" of the gsd-openclaw skill - the SKILL.md definition that OpenClaw loads, the response formatting layer that adapts output for Feishu's Card Message format, and the prompt templates for context recovery, command mapping, and error handling. This phase focuses exclusively on Feishu integration as the primary channel.

Primary recommendation: Use Feishu Card JSON 2.0 structure with three-zone layout (header/content/actions). Keep prompt templates in pure Markdown with {{variable}} syntax for simple string replacement (no Jinja2 dependency needed).

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • 触发模式: Only /gsd:* commands, no natural language triggers
  • 命令匹配: Wildcard /gsd:* pattern, orchestrator.py validates command validity
  • 项目上下文: Explicit switch via /gsd:use <project> to set current project
  • 支持频道: Feishu only (Phase 2)
  • 消息格式: Feishu Card Message
  • 交互元素:
    • Interactive buttons (for approval confirmation, operation triggers)
    • Dropdown selectors (for project selection)
    • Link buttons (jump to OpenClaw control panel)
  • 卡片布局规范: Three-zone structure (title + content + actions)
  • 长消息处理: Pagination with [1/N] markers at message end
  • 错误消息格式: Error cards with red theme + guidance buttons
  • GSD审批流程: Feishu button interaction for approve/reject
  • Prompt模板类型: context_recovery.md, command_mapping.md, error_recovery.md
  • 模板格式: Markdown with {{variable}} syntax

Claude's Discretion

  • Exact character limit per message (based on Feishu API docs)
  • Code block rendering in cards
  • Card color themes and icon selection
  • Pagination break points (avoid truncating code blocks or tables)

Deferred Ideas (OUT OF SCOPE)

  • WeChat channel support (Phase 3+)
  • Telegram/Discord channel support (Phase 3+)
  • Multi-language support (Chinese/English switching)
  • Custom card themes

</user_constraints>

<phase_requirements>

Phase Requirements

ID Description Research Support
OCLW-01 SKILL.md has proper frontmatter (name, description, triggers) SKILL.md YAML frontmatter format documented below; use name and description fields
OCLW-02 Skill triggers on /gsd:* patterns and project management keywords OpenClaw matches skill based on description field content; use explicit /gsd: in description
OCLW-04 Response formatting adapts to channel capabilities (markdown vs plain) Feishu Card JSON 2.0 structure documented; response_formatter.py pattern established

</phase_requirements>

Standard Stack

Core

Library Version Purpose Why Standard
Python 3.9+ Core language Required by existing codebase
dataclasses stdlib Data structures Used in Phase 1, consistent pattern
json stdlib JSON handling Card message structure
pathlib stdlib Path handling Template file loading
re stdlib Pattern matching Command validation

Supporting

Library Version Purpose When to Use
string.Template stdlib Simple variable substitution Prompt templates (no Jinja2 needed)
typing stdlib Type hints API contracts

Alternatives Considered

Instead of Could Use Tradeoff
string.Template Jinja2 Jinja2 adds dependency for simple {{var}} replacement; Template is sufficient
Custom card builder Feishu Card Builder API Card Builder is visual tool; programmatic JSON is more maintainable

No new pip dependencies required - all Phase 2 components use Python standard library.

Architecture Patterns

src/gsd_openclaw/
├── __init__.py
├── state_manager.py      # Phase 1 - existing
├── tmux_session.py       # Phase 1 - existing
├── workspace.py          # Phase 1 - existing
├── cli.py                # Phase 1 - existing
├── response_formatter.py # Phase 2 - NEW
└── prompts/              # Phase 2 - NEW
    ├── __init__.py
    ├── context_recovery.md
    ├── command_mapping.md
    └── error_recovery.md

skill/                    # Phase 2 - NEW (deployment package)
├── SKILL.md              # OpenClaw skill definition
├── orchestrator.py       # Phase 3 - placeholder
└── prompts/              # Symlink or copy from src/gsd_openclaw/prompts/

Pattern 1: SKILL.md Frontmatter Format

What: YAML frontmatter between --- delimiters defines skill metadata.

When to use: Always - this is required for OpenClaw to load the skill.

Example:

---
name: gsd-openclaw
description: GSD project management via /gsd:* commands. Triggers on /gsd:new-project, /gsd:plan, /gsd:status, /gsd:use and other GSD slash commands.
---

# GSD-OpenClaw Skill

## When to Use

This skill triggers when users send messages starting with `/gsd:` ...

## Commands

| Command | Description |
|---------|-------------|
| `/gsd:new-project <name>` | Create a new GSD project |
| `/gsd:use <project>` | Switch to a project context |
...

Source: OpenClaw skills pattern from Creating Custom Skills - only name and description fields are required. The description field serves as the trigger condition.

Pattern 2: Feishu Card JSON 2.0 Structure

What: Feishu Card messages use JSON 2.0 schema with three main sections.

When to use: Every response sent to Feishu.

Example:

{
  "schema": "2.0",
  "config": {
    "wide_screen_mode": true,
    "update_multi": true
  },
  "header": {
    "title": {
      "tag": "plain_text",
      "content": "GSD: new-project"
    },
    "template": "blue"
  },
  "elements": [
    {
      "tag": "div",
      "text": {
        "tag": "lark_md",
        "content": "Project **my-auth** created successfully."
      }
    }
  ]
}

Three-zone layout:

  1. header: Title + status color (blue=info, green=success, red=error)
  2. elements: Main content area (div, markdown, note components)
  3. actions: Interactive buttons (at card bottom)

Source: Feishu Card JSON 2.0 Structure

Pattern 3: Interactive Button with Callback

What: Buttons trigger callbacks to the skill with custom value data.

When to use: Approval flows, action confirmations.

Example:

{
  "tag": "action",
  "actions": [
    {
      "tag": "button",
      "text": {
        "tag": "plain_text",
        "content": "Confirm"
      },
      "type": "primary",
      "value": {
        "action": "approve",
        "project": "my-auth",
        "phase": "3"
      }
    },
    {
      "tag": "button",
      "text": {
        "tag": "plain_text",
        "content": "Cancel"
      },
      "type": "default",
      "value": {
        "action": "reject",
        "project": "my-auth"
      }
    }
  ]
}

Source: Feishu Button Component

Pattern 4: Prompt Template with Variable Substitution

What: Markdown templates with {{variable}} placeholders.

When to use: Context recovery, error messages, command mapping.

Example (context_recovery.md):

# Context Recovery

You are resuming a GSD session that was interrupted.

## Session Information
- **Project**: {{project_name}}
- **Last Active**: {{last_active_at}}
- **Current Phase**: {{current_phase}}
- **Commands Executed**: {{command_count}}

## Last Known State
{{last_output_snapshot}}

## Instructions
Continue from where the session left off. Do not repeat completed steps.

Implementation:

from string import Template
from pathlib import Path

def load_template(name: str) -> Template:
    """Load a prompt template by name."""
    template_path = Path(__file__).parent / "prompts" / f"{name}.md"
    return Template(template_path.read_text(encoding='utf-8'))

def render_template(name: str, **variables) -> str:
    """Render a template with variables."""
    template = load_template(name)
    return template.safe_substitute(**variables)

Note: Use safe_substitute() to leave unmatched variables as-is instead of raising KeyError.

Pattern 5: Response Formatter Class

What: Centralized formatting for all Feishu responses.

When to use: Every response going to Feishu channel.

Example:

from dataclasses import dataclass
from typing import List, Optional, Dict, Any
from enum import Enum

class CardTheme(Enum):
    SUCCESS = "green"
    ERROR = "red"
    INFO = "blue"
    WARNING = "orange"

@dataclass
class CardButton:
    text: str
    value: Dict[str, Any]
    button_type: str = "primary"  # primary, default, danger

class FeishuResponseFormatter:
    """Format responses as Feishu Card messages."""

    def format_success(self, title: str, content: str,
                       buttons: Optional[List[CardButton]] = None) -> dict:
        """Format a success response card."""
        return self._build_card(title, content, CardTheme.SUCCESS, buttons)

    def format_error(self, title: str, content: str,
                     buttons: Optional[List[CardButton]] = None) -> dict:
        """Format an error response card."""
        return self._build_card(title, content, CardTheme.ERROR, buttons)

    def format_paginated(self, title: str, content: str,
                         page: int, total_pages: int) -> dict:
        """Format a paginated response."""
        footer = f"\n\n---\n[{page}/{total_pages}]"
        return self._build_card(title, content + footer, CardTheme.INFO)

    def _build_card(self, title: str, content: str, theme: CardTheme,
                    buttons: Optional[List[CardButton]] = None) -> dict:
        """Build complete card JSON structure."""
        card = {
            "schema": "2.0",
            "config": {"wide_screen_mode": True},
            "header": {
                "title": {"tag": "plain_text", "content": title},
                "template": theme.value
            },
            "elements": [
                {"tag": "div", "text": {"tag": "lark_md", "content": content}}
            ]
        }
        if buttons:
            card["elements"].append(self._build_actions(buttons))
        return card

    def _build_actions(self, buttons: List[CardButton]) -> dict:
        """Build actions element from button list."""
        return {
            "tag": "action",
            "actions": [
                {
                    "tag": "button",
                    "text": {"tag": "plain_text", "content": btn.text},
                    "type": btn.button_type,
                    "value": btn.value
                }
                for btn in buttons
            ]
        }

Anti-Patterns to Avoid

  • Hardcoding full card JSON in multiple places: Use ResponseFormatter class consistently
  • Jinja2 for simple templates: string.Template is sufficient for {{var}} syntax
  • Exceeding 30KB card size: Paginate long outputs (card JSON body max 30KB)
  • Mixing card schema versions: Always use "schema": "2.0" consistently

Don't Hand-Roll

Problem Don't Build Use Instead Why
Card JSON construction String concatenation ResponseFormatter class Type safety, consistency
Template variable substitution regex replace string.Template Edge cases (escaped braces)
Markdown to Feishu format Custom converter lark_md tag Feishu has built-in markdown support

Key insight: Feishu's lark_md tag supports standard markdown - no conversion needed. Just wrap markdown content in {"tag": "lark_md", "content": "..."}.

Common Pitfalls

Pitfall 1: Card Size Exceeded

What goes wrong: Feishu rejects messages > 30KB JSON body. Why it happens: Long GSD outputs (especially with code blocks) can exceed limit. How to avoid: Implement pagination in ResponseFormatter. Check content size before sending. Warning signs: The length of the message content reaches its limit error.

Pitfall 2: Missing Button Callback Value

What goes wrong: Button clicks don't trigger expected action. Why it happens: value field missing or improperly formatted in button JSON. How to avoid: Always include value dict with action metadata. Test button clicks manually. Warning signs: Button renders but clicking produces no response.

Pitfall 3: Template Variable Not Substituted

What goes wrong: {{variable}} appears literally in output. Why it happens: Variable name mismatch or using substitute() instead of safe_substitute(). How to avoid: Use safe_substitute(), validate template variables in tests. Warning signs: Raw {{...}} strings in generated prompts.

Pitfall 4: Wrong Card Schema Version

What goes wrong: Card components don't render correctly. Why it happens: Using JSON 1.0 structure with 2.0 expectations. How to avoid: Always include "schema": "2.0" at card root. Check client version >= 7.20. Warning signs: Missing elements, broken layouts.

Code Examples

Complete Feishu Card Example (Success with Buttons)

# Source: Based on Feishu Card JSON 2.0 documentation
# https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-structure

{
    "schema": "2.0",
    "config": {
        "wide_screen_mode": True,
        "update_multi": True
    },
    "header": {
        "title": {
            "tag": "plain_text",
            "content": "GSD: Phase 3 Ready for Review"
        },
        "template": "blue"
    },
    "elements": [
        {
            "tag": "div",
            "text": {
                "tag": "lark_md",
                "content": "**Project**: my-auth\n**Phase**: 3 - Orchestration\n**Status**: Planning complete, awaiting approval\n\n**Summary**:\n- 4 tasks defined\n- Estimated time: 2 hours\n- Dependencies: Phase 1 (complete)"
            }
        },
        {
            "tag": "hr"
        },
        {
            "tag": "action",
            "actions": [
                {
                    "tag": "button",
                    "text": {"tag": "plain_text", "content": "Approve"},
                    "type": "primary",
                    "value": {"action": "approve", "project": "my-auth", "phase": "3"}
                },
                {
                    "tag": "button",
                    "text": {"tag": "plain_text", "content": "View Details"},
                    "type": "default",
                    "value": {"action": "details", "project": "my-auth"}
                }
            ]
        }
    ]
}

Error Card Example

# Error card with guidance buttons

{
    "schema": "2.0",
    "config": {"wide_screen_mode": True},
    "header": {
        "title": {"tag": "plain_text", "content": "Error: Project Not Found"},
        "template": "red"
    },
    "elements": [
        {
            "tag": "div",
            "text": {
                "tag": "lark_md",
                "content": "Project `nonexistent-project` does not exist.\n\n**Suggestion**: Create a new project or select an existing one."
            }
        },
        {
            "tag": "action",
            "actions": [
                {
                    "tag": "button",
                    "text": {"tag": "plain_text", "content": "Create New"},
                    "type": "primary",
                    "value": {"action": "create_project", "name": "nonexistent-project"}
                },
                {
                    "tag": "button",
                    "text": {"tag": "plain_text", "content": "My Projects"},
                    "type": "default",
                    "value": {"action": "list_projects"}
                }
            ]
        }
    ]
}

Template Loading and Rendering

# Source: Python standard library pattern
from string import Template
from pathlib import Path
from typing import Dict, Any

class PromptTemplateLoader:
    """Load and render prompt templates."""

    def __init__(self, template_dir: Path):
        self.template_dir = template_dir
        self._cache: Dict[str, Template] = {}

    def get(self, name: str) -> Template:
        """Get a template by name (cached)."""
        if name not in self._cache:
            path = self.template_dir / f"{name}.md"
            if not path.exists():
                raise FileNotFoundError(f"Template not found: {name}")
            self._cache[name] = Template(path.read_text(encoding='utf-8'))
        return self._cache[name]

    def render(self, name: str, **variables: Any) -> str:
        """Render a template with variables."""
        template = self.get(name)
        return template.safe_substitute(**variables)

# Usage
loader = PromptTemplateLoader(Path(__file__).parent / "prompts")
prompt = loader.render("context_recovery",
    project_name="my-auth",
    last_active_at="2026-03-11T10:00:00Z",
    current_phase="3",
    command_count="15",
    last_output_snapshot="Last task completed: orchestrator.py scaffold"
)

State of the Art

Old Approach Current Approach When Changed Impact
Feishu Card JSON 1.0 Card JSON 2.0 Client 7.20+ Better structure, streaming updates
Hardcoded JSON strings Card Builder + Templates 2025+ Maintainability, consistency
Plain text responses Card messages OpenClaw standard Rich UI, interactivity

Deprecated/outdated:

  • Feishu Card JSON 1.0: Use 2.0 schema ("schema": "2.0" required)
  • Raw text messages: Always use card format for structured responses

Open Questions

  1. Pagination break point algorithm

    • What we know: Cards max 30KB, need to avoid mid-code-block splits
    • What's unclear: Best algorithm for smart break points
    • Recommendation: Split at paragraph boundaries; if code block > limit, split at line boundaries with continuation marker
  2. Button callback endpoint configuration

    • What we know: Feishu sends callbacks to configured URL
    • What's unclear: How OpenClaw routes callbacks to skill (Phase 3 concern)
    • Recommendation: Design button values to be self-contained (include all needed context)

Validation Architecture

Test Framework

Property Value
Framework pytest (existing from Phase 1)
Config file None - conftest.py pattern established
Quick run command pytest tests/test_response_formatter.py -v
Full suite command pytest tests/ -v

Phase Requirements -> Test Map

Req ID Behavior Test Type Automated Command File Exists?
OCLW-01 SKILL.md has proper frontmatter unit pytest tests/test_skill_md.py -v Wave 0
OCLW-02 Skill triggers on /gsd:* patterns unit pytest tests/test_skill_md.py::test_trigger_pattern -v Wave 0
OCLW-04 Response formatting adapts to channel unit pytest tests/test_response_formatter.py -v Wave 0

Sampling Rate

  • Per task commit: pytest tests/test_response_formatter.py tests/test_skill_md.py -v
  • Per wave merge: pytest tests/ -v
  • Phase gate: Full suite green before /gsd:verify-work

Wave 0 Gaps

  • tests/test_response_formatter.py - tests for FeishuResponseFormatter class
  • tests/test_skill_md.py - tests for SKILL.md structure and trigger patterns
  • tests/test_prompt_templates.py - tests for template loading and rendering
  • src/gsd_openclaw/prompts/ directory - template files need creation

Test Patterns to Follow

# tests/test_response_formatter.py - Pattern from test_state_manager.py

import pytest
from gsd_openclaw.response_formatter import (
    FeishuResponseFormatter, CardTheme, CardButton
)

class TestFeishuResponseFormatter:
    """Test response formatting for Feishu cards."""

    def test_format_success_returns_valid_card(self):
        """Success card has green theme."""
        formatter = FeishuResponseFormatter()
        card = formatter.format_success("Test", "Content")

        assert card["schema"] == "2.0"
        assert card["header"]["template"] == "green"
        assert card["header"]["title"]["content"] == "Test"

    def test_format_error_returns_red_theme(self):
        """Error card has red theme."""
        formatter = FeishuResponseFormatter()
        card = formatter.format_error("Error", "Something went wrong")

        assert card["header"]["template"] == "red"

    def test_format_with_buttons_includes_actions(self):
        """Cards with buttons include action element."""
        formatter = FeishuResponseFormatter()
        buttons = [CardButton("OK", {"action": "ok"})]
        card = formatter.format_success("Test", "Content", buttons=buttons)

        actions = [e for e in card["elements"] if e["tag"] == "action"]
        assert len(actions) == 1
        assert actions[0]["actions"][0]["text"]["content"] == "OK"

Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)

Metadata

Confidence breakdown:

  • Standard stack: HIGH - Python stdlib only, no new dependencies
  • Architecture: HIGH - Based on official Feishu docs and existing Phase 1 patterns
  • Pitfalls: HIGH - Documented from official API limits and common integration issues

Research date: 2026-03-11 Valid until: 30 days (stable APIs, but Feishu may update card schema)