Agent Skills › kinncj/Heimdall

kinncj/Heimdall

GitHub

对生成的UI进行WCAG 2.2 AA级无障碍审计。支持axe-core或pa11y工具,自动检测并运行检查,将违规项发布为PR评论并阻止合并。针对tui目标则使用终端清单评估,确保所有ui: true的故事符合无障碍标准。

105 skills 50

Install All Skills

npx skills add kinncj/Heimdall --all -g -y
More Options

List skills in collection

npx skills add kinncj/Heimdall --list

Skills in Collection (105)

对生成的UI进行WCAG 2.2 AA级无障碍审计。支持axe-core或pa11y工具,自动检测并运行检查,将违规项发布为PR评论并阻止合并。针对tui目标则使用终端清单评估,确保所有ui: true的故事符合无障碍标准。
故事包含 ui:true 属性时 用户明确要求审计无障碍性时
.claude/skills/a11y-audit/SKILL.md
npx skills add kinncj/Heimdall --skill a11y-audit -g -y
SKILL.md
Frontmatter
{
    "name": "a11y-audit",
    "description": "Run WCAG 2.2 Level AA accessibility audits against generated UI. Use when a story has ui:true or when asked to audit accessibility."
}

SKILL: a11y-audit

Purpose

Run WCAG 2.2 Level AA accessibility audits against generated UI. Uses axe-core (via @axe-core/cli) or pa11y as available. Posts findings as PR comments. Blocks merge on AA violations. Required for every story with ui: true.

Target awareness

When design.target (project.config.yaml, default web) is tui, skip the browser tooling (axe/pa11y) and the preview URL. Instead evaluate the terminal a11y checklist from docs/design/design-targets.md and write the findings to docs/design/mockups/<id>.a11y.json using the SAME object shape the web path produces — an object with violations[], where each violation has id, impact (critical|serious|moderate|minor), description, help, and nodes[] ({target:[...], failureSummary}). The gate (scripts/sdlc/a11y-gate.sh) reads only violations[].impact, so matching this shape keeps it unchanged. Map checklist failures to impacts: keyboard-reachable→critical, focus-visible→serious, color-contrast→serious, color-only-signaling→serious, no-color-support→moderate, min-width-resize→moderate. The WCAG criteria, tool-detection, axe/pa11y, and PR-comment sections below apply to web targets.

WCAG 2.2 AA — Minimum Requirements

Criterion Level What to check
1.1.1 Non-text Content A All <img> have alt. Icons used as UI controls have labels.
1.3.1 Info and Relationships A Semantic HTML: headings, lists, tables, landmarks used correctly.
1.4.3 Contrast (minimum) AA Normal text ≥ 4.5:1. Large text ≥ 3:1.
1.4.11 Non-text Contrast AA UI components and focus indicators ≥ 3:1 against background.
2.1.1 Keyboard A All interactive elements reachable and operable via keyboard.
2.4.3 Focus Order A Tab order is logical and follows visual layout.
2.4.7 Focus Visible AA Focus indicator visible on all interactive elements.
3.3.1 Error Identification A Errors identified in text, not color alone.
3.3.2 Labels or Instructions A All inputs have labels.
4.1.2 Name, Role, Value A All UI components have accessible name, role, and state.

Tool Detection

detect_tool() {
  if command -v axe &>/dev/null; then
    echo "axe"
  elif command -v pa11y &>/dev/null; then
    echo "pa11y"
  elif npx --yes @axe-core/cli --version &>/dev/null 2>&1; then
    echo "axe-npx"
  else
    echo "none"
  fi
}
TOOL=$(detect_tool)

Run with axe-core CLI

URL="${1:-http://localhost:3000}"  # preview URL or Storybook story URL
STORY_ID="${2:-unknown}"
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

mkdir -p docs/design/mockups

axe "$URL" \
  --stdout \
  --tags wcag2a,wcag2aa,wcag21aa,wcag22aa \
  > "$REPORT"

VIOLATIONS=$(python3 -c "import json,sys; d=json.load(open('$REPORT')); print(len(d.get('violations',[])))")
echo "[a11y-audit] axe  $URL  violations=$VIOLATIONS"

Run with pa11y

URL="${1:-http://localhost:3000}"
STORY_ID="${2:-unknown}"
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

pa11y "$URL" \
  --standard WCAG2AA \
  --reporter json \
  > "$REPORT" 2>&1

ISSUES=$(python3 -c "import json,sys; d=json.load(open('$REPORT')); print(len(d) if isinstance(d,list) else 0)")
echo "[a11y-audit] pa11y  $URL  issues=$ISSUES"

Parse Results and Classify

import json, sys

report_path = sys.argv[1]
data = json.load(open(report_path))

# axe format
violations = data.get("violations", [])
passes     = data.get("passes", [])

critical = [v for v in violations if v["impact"] in ("critical", "serious")]
moderate = [v for v in violations if v["impact"] == "moderate"]
minor    = [v for v in violations if v["impact"] == "minor"]

print(f"CRITICAL/SERIOUS: {len(critical)}")
print(f"MODERATE:         {len(moderate)}")
print(f"MINOR:            {len(minor)}")
print(f"PASSES:           {len(passes)}")

# Merge gate: block on critical + serious
if critical:
    print("\nMERGE BLOCKED — resolve the following before merging:")
    for v in critical:
        print(f"  [{v['impact'].upper()}] {v['id']}: {v['description']}")
        for node in v.get("nodes", [])[:2]:
            print(f"    → {node.get('target', ['?'])[0]}: {node.get('failureSummary','')[:120]}")
    sys.exit(1)

Post Findings as PR Comment

STORY_ID="auth-reset-0001"
PR_NUMBER=$(gh pr list --head "$(git branch --show-current)" --json number --jq '.[0].number')
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

# Build comment body
COMMENT=$(python3 - <<'EOF'
import json, sys

data = json.load(open(sys.argv[1]))
violations = data.get("violations", [])

if not violations:
    print("## ✅ A11y Audit Passed\n\nNo WCAG 2.2 AA violations found.")
    sys.exit(0)

lines = [f"## ⚠️ A11y Audit: {len(violations)} violation(s) found\n"]
for v in violations[:10]:
    impact = v.get("impact","").upper()
    lines.append(f"### [{impact}] {v['id']}")
    lines.append(f"_{v['description']}_\n")
    for node in v.get("nodes",[])[:2]:
        selector = ', '.join(node.get("target",[]))
        lines.append(f"- `{selector}`")
        lines.append(f"  {node.get('failureSummary','')[:200]}")
    lines.append("")

if len(violations) > 10:
    lines.append(f"_...and {len(violations)-10} more. See full report in `docs/design/mockups/`._")

print('\n'.join(lines))
EOF
"$REPORT")

gh pr comment "$PR_NUMBER" --body "$COMMENT"
echo "[a11y-audit] PR_COMMENT  #$PR_NUMBER  violations=$(echo "$COMMENT" | grep -c '\[CRITICAL\]\|\[SERIOUS\]' || echo 0)"

Merge Gate Check

Called from scripts/sdlc/a11y-gate.sh (added in Phase VII):

STORY_FILE="$1"
UI=$(python3 -c "
import re
m = re.search(r'^ui:\s*(true|false)', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'false')
")

if [ "$UI" != "true" ]; then
  echo "[a11y-audit] SKIP  ui:false story — no audit required"
  exit 0
fi

STORY_ID=$(python3 -c "
import re
m = re.search(r'^id:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

if [ ! -f "$REPORT" ]; then
  echo "[a11y-audit] FAIL  no audit report found for $STORY_ID — run audit before merging"
  exit 1
fi

python3 scripts/sdlc/parse-a11y-report.py "$REPORT"

Manual Audit Checklist (no tool available)

When TOOL=none, perform a structured manual check:

## Manual A11y Checklist — {story_id}

### Perceivable
- [ ] All images have meaningful `alt` text (or `alt=""` for decorative)
- [ ] Color is not the sole means of conveying information
- [ ] Normal text contrast ≥ 4.5:1 (check with browser DevTools)
- [ ] Large text contrast ≥ 3:1

### Operable
- [ ] Tab through the entire form/component with keyboard only
- [ ] All actions reachable without mouse
- [ ] No keyboard trap
- [ ] Focus indicator visible on every interactive element

### Understandable
- [ ] Form inputs have visible labels (not just placeholders)
- [ ] Errors described in text, not color alone
- [ ] Error messages are specific and actionable

### Robust
- [ ] Correct semantic elements: `<button>`, `<input>`, `<label>`
- [ ] ARIA attributes only where native HTML is insufficient
- [ ] Works with browser zoom at 200%

Failure Modes

Condition Action
No tool available Fall back to manual checklist. Log TOOL=none — manual audit required.
Preview URL not reachable Log URL_UNREACHABLE. Do not skip audit — escalate.
ui: false story Skip audit. Log SKIP — ui:false.
Critical violations found Post PR comment. Exit non-zero. Block merge.
Report already exists and is current Skip re-run. Log SKIP — current report exists.

Logging

[a11y-audit] RUN      http://localhost:6006/... story=auth-reset-0001  tool=axe
[a11y-audit] RESULT   violations=0  passes=24  WCAG2AA=PASS
[a11y-audit] RESULT   violations=3  critical=1  WCAG2AA=FAIL
[a11y-audit] PR_COMMENT  #42  violations=1
[a11y-audit] SKIP     ui:false story — audit not required
[a11y-audit] BLOCKED  merge blocked — 1 critical violation unresolved
根据设计令牌和模拟文件,自动生成包含实现、Storybook故事、单元测试及Gherkin规范的完整UI组件骨架。支持React-Mantine等框架,从项目配置自动检测技术栈,生成可运行的TODO占位代码。
需要创建新的UI组件 为现有组件生成测试和规范文件
.claude/skills/component-scaffold/SKILL.md
npx skills add kinncj/Heimdall --skill component-scaffold -g -y
SKILL.md
Frontmatter
{
    "name": "component-scaffold",
    "description": "Generate a complete component file tree wired to design tokens with tests and Gherkin spec. Use when scaffolding a new UI component."
}

SKILL: component-scaffold

Purpose

Generate a complete component file tree wired to design tokens. Each component gets an implementation file, Storybook story, unit test, and Gherkin spec file. Skeletons are runnable with TODO stubs — not empty files. Stack is auto-detected from project.config.yaml.

Inputs

Field Source Example
component_name PascalCase PasswordResetForm
story_id story frontmatter id auth-reset-0001
tokens_file docs/design/identity/tokens.json required
stack project.config.yaml react-mantine | react-tailwind | html
mockup_file approved mockup docs/design/mockups/auth-reset-0001.mockup.tsx

Output Tree

For PasswordResetForm with stack react-mantine:

app/
└── components/
    └── PasswordResetForm/
        ├── index.tsx              ← component implementation
        ├── PasswordResetForm.stories.tsx  ← Storybook story
        ├── PasswordResetForm.test.tsx     ← unit tests (Vitest / Jest)
        └── PasswordResetForm.spec.ts      ← Gherkin step binding

Generate Component Index (react-mantine)

app/components/{ComponentName}/index.tsx:

// {ComponentName} — generated from story {story_id}
// Tokens: docs/design/identity/mantine.theme.ts
// TODO: implement from mockup docs/design/mockups/{story_id}.mockup.tsx

import { Stack, TextInput, Button } from '@mantine/core';
import { useState } from 'react';

export interface {ComponentName}Props {
  // TODO: define props from story acceptance criteria
  onSubmit?: (data: Record<string, unknown>) => void;
}

export function {ComponentName}({ onSubmit }: {ComponentName}Props) {
  // TODO: implement
  return (
    <Stack>
      {/* TODO: build from approved mockup */}
    </Stack>
  );
}

export default {ComponentName};

Generate Storybook Story

app/components/{ComponentName}/{ComponentName}.stories.tsx:

import type { Meta, StoryObj } from '@storybook/react';
import { {ComponentName} } from '.';

const meta: Meta<typeof {ComponentName}> = {
  title: 'Components/{ComponentName}',
  component: {ComponentName},
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof {ComponentName}>;

export const Default: Story = {
  args: {},
};

export const WithError: Story = {
  args: {
    // TODO: add error props
  },
};

Generate Unit Test

app/components/{ComponentName}/{ComponentName}.test.tsx:

import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { {ComponentName} } from '.';

describe('{ComponentName}', () => {
  it('renders without crashing', () => {
    render(<{ComponentName} />);
    // TODO: assert presence of key elements from wireframe
    expect(document.body).toBeTruthy();
  });

  it('calls onSubmit when form is submitted', () => {
    // TODO: implement interaction test
  });

  it('shows validation error when input is invalid', () => {
    // TODO: implement error state test
  });
});

Generate Gherkin Step Binding

app/components/{ComponentName}/{ComponentName}.spec.ts:

// Gherkin step bindings for story {story_id}
// Feature file: tests/features/{epic}/{story_slug}.feature
import { Given, When, Then } from '@cucumber/cucumber';

// TODO: copy matching steps from cucumber-automation output
// Example:
// Given('the user is on the {string} page', async (page: string) => {
//   throw new Error('Pending');
// });

Python Stack Alternative

For stack=python (e.g., Django / HTMX):

app/
└── components/
    └── password_reset_form/
        ├── __init__.py
        ├── template.html
        ├── test_password_reset_form.py
        └── password_reset_form_steps.py

Scaffold Script

COMPONENT="PasswordResetForm"
STORY_ID="auth-reset-0001"
STACK="react-mantine"
DIR="app/components/$COMPONENT"

mkdir -p "$DIR"

# Check for existing files — never overwrite
for f in "index.tsx" "${COMPONENT}.stories.tsx" "${COMPONENT}.test.tsx" "${COMPONENT}.spec.ts"; do
  if [ -f "$DIR/$f" ]; then
    echo "[component-scaffold] SKIP  $DIR/$f  (already exists)"
  else
    echo "[component-scaffold] CREATE  $DIR/$f"
    # write the appropriate template (see above)
  fi
done

Token Wiring

After scaffold, inject token import into index.tsx:

# Verify mantine.theme.ts exists
[ -f "docs/design/identity/mantine.theme.ts" ] || \
  echo "[component-scaffold] WARN  mantine.theme.ts missing — run design-tokens skill"

Failure Modes

Condition Action
ComponentName not PascalCase Warn and convert: password-reset-formPasswordResetForm.
app/components/ does not exist Create it. Log the creation.
File already exists Skip with SKIP log — never overwrite existing implementations.
tokens.json missing Scaffold without token import. Add // TODO: wire to design tokens comment.
Stack unknown Default to react-mantine. Log warning.

Logging

[component-scaffold] CREATE  app/components/PasswordResetForm/index.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.stories.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.test.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.spec.ts
[component-scaffold] SKIP    app/components/PasswordResetForm/index.tsx  (already exists)
[component-scaffold] WARN    mantine.theme.ts missing
从故事Markdown文件提取Gherkin场景生成可运行的.feature文件,并根据项目技术栈自动生成步骤定义存根。支持TypeScript、JavaScript、Python和Java,确保测试套件与文档同步。
需要将故事文档同步为自动化测试用例 初始化或更新Cucumber/BDD测试套件结构 生成缺失的步骤定义代码骨架
.claude/skills/cucumber-automation/SKILL.md
npx skills add kinncj/Heimdall --skill cucumber-automation -g -y
SKILL.md
Frontmatter
{
    "name": "cucumber-automation",
    "description": "Extract Gherkin scenarios from story markdown files into runnable .feature files and generate step definition stubs. Use when syncing stories to test suites."
}

SKILL: cucumber-automation

Purpose

Extract Gherkin scenarios from story markdown files into runnable .feature files, and generate step definition stubs for the project's test stack. Keeps tests/features/ in sync with docs/stories/ as the single source of Gherkin truth.

Supported Stacks

Stack Step file extension Framework
TypeScript / Node.js .steps.ts @cucumber/cucumber
JavaScript / Node.js .steps.js @cucumber/cucumber
Python _steps.py behave
Java Steps.java Cucumber-JVM

Detect the stack from the repo root:

if [ -f "package.json" ] && grep -q "@cucumber/cucumber" package.json 2>/dev/null; then
  STACK="typescript"
elif [ -f "requirements.txt" ] && grep -q "behave" requirements.txt 2>/dev/null; then
  STACK="python"
elif [ -f "pom.xml" ] || [ -f "build.gradle" ]; then
  STACK="java"
else
  STACK="unknown"
fi

Extract Gherkin from a Story File

Story files contain Gherkin in fenced code blocks tagged with gherkin:

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"
EPIC="user-auth"

# Extract content of ```gherkin ... ``` blocks
python3 - <<'EOF'
import re, sys

story = open(sys.argv[1]).read()
blocks = re.findall(r'```gherkin\n(.*?)```', story, re.DOTALL)
if not blocks:
    print("NO_GHERKIN", file=sys.stderr)
    sys.exit(1)
print('\n\n'.join(blocks))
EOF "$STORY_FILE"

Write the Feature File

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"
FEATURE_DIR="tests/features"
EPIC="user-auth"

mkdir -p "$FEATURE_DIR/$EPIC"
FEATURE_FILE="$FEATURE_DIR/$EPIC/reset-password.feature"

# Extract and write (idempotent — overwrites if Gherkin changed)
python3 - <<'EOF'
import re, sys, os

story_path, feature_path = sys.argv[1], sys.argv[2]
story = open(story_path).read()
blocks = re.findall(r'```gherkin\n(.*?)```', story, re.DOTALL)
if not blocks:
    print(f"[cucumber] NO_GHERKIN in {story_path}", file=sys.stderr)
    sys.exit(1)

os.makedirs(os.path.dirname(feature_path), exist_ok=True)
with open(feature_path, 'w') as f:
    f.write('\n\n'.join(b.rstrip() for b in blocks))
    f.write('\n')

print(f"[cucumber] WRITE  {feature_path}  scenarios={len(re.findall(r'^  Scenario', ''.join(blocks), re.MULTILINE))}")
EOF "$STORY_FILE" "$FEATURE_FILE"

Generate Step Definition Stubs (TypeScript)

FEATURE_FILE="tests/features/user-auth/reset-password.feature"
STEPS_DIR="tests/step-definitions/user-auth"
mkdir -p "$STEPS_DIR"

python3 - <<'EOF'
import re, sys, os

feature_path = sys.argv[1]
steps_dir = sys.argv[2]
feature = open(feature_path).read()

# Collect unique step texts
steps = re.findall(r'^\s+(Given|When|Then|And|But) (.+)$', feature, re.MULTILINE)
seen = set()
unique = []
for keyword, text in steps:
    norm = re.sub(r'"[^"]+"', '"{string}"', text)
    norm = re.sub(r'\b\d+\b', '{int}', norm)
    if norm not in seen:
        seen.add(norm)
        unique.append((keyword if keyword not in ('And','But') else 'Given', norm, text))

# Build TypeScript file
feature_name = os.path.basename(feature_path).replace('.feature','')
out_path = os.path.join(steps_dir, f"{feature_name}.steps.ts")

if os.path.exists(out_path):
    print(f"[cucumber] SKIP  {out_path}  (already exists)")
    sys.exit(0)

lines = [
    "import { Given, When, Then } from '@cucumber/cucumber';",
    "",
]
for keyword, pattern, original in unique:
    fn = keyword.lower()
    # build parameter list from placeholders
    params = []
    if '{string}' in pattern:
        params += [f"arg{i}: string" for i in range(pattern.count('{string}'))]
    if '{int}' in pattern:
        params += [f"n{i}: number" for i in range(pattern.count('{int}'))]
    param_str = ', '.join(params)
    escaped = pattern.replace("'", "\\'")
    lines += [
        f"// {original}",
        f"{fn}('{escaped}', async ({param_str}) => {{",
        f"  // TODO: implement",
        f"  throw new Error('Pending: {escaped}');",
        f"}});",
        "",
    ]

with open(out_path, 'w') as f:
    f.write('\n'.join(lines))

print(f"[cucumber] STUBS  {out_path}  steps={len(unique)}")
EOF "$FEATURE_FILE" "$STEPS_DIR"

Generate Step Definition Stubs (Python / behave)

FEATURE_FILE="tests/features/user_auth/reset_password.feature"
STEPS_DIR="tests/features/user_auth"
mkdir -p "$STEPS_DIR"

python3 - <<'EOF'
import re, sys, os

feature_path = sys.argv[1]
steps_dir = sys.argv[2]
feature = open(feature_path).read()

steps = re.findall(r'^\s+(Given|When|Then|And|But) (.+)$', feature, re.MULTILINE)
seen = set()
unique = []
for keyword, text in steps:
    norm = re.sub(r'"[^"]+"', '"{text}"', text)
    norm = re.sub(r'\b\d+\b', '{n:d}', norm)
    if norm not in seen:
        seen.add(norm)
        unique.append((keyword if keyword not in ('And','But') else 'given', norm, text))

feature_name = os.path.basename(feature_path).replace('.feature','')
out_path = os.path.join(steps_dir, f"{feature_name}_steps.py")

if os.path.exists(out_path):
    print(f"[cucumber] SKIP  {out_path}  (already exists)")
    sys.exit(0)

lines = ["from behave import given, when, then", ""]
for keyword, pattern, original in unique:
    fn = keyword.lower()
    escaped = pattern.replace('"', '\\"')
    lines += [
        f"# {original}",
        f'@{fn}(u"{escaped}")',
        f"def step_impl(context):",
        f'    raise NotImplementedError(u"STEP: {fn} {escaped}")',
        "",
    ]

with open(out_path, 'w') as f:
    f.write('\n'.join(lines))

print(f"[cucumber] STUBS  {out_path}  steps={len(unique)}")
EOF "$FEATURE_FILE" "$STEPS_DIR"

Batch Sync (all stories → all feature files)

find docs/stories -name "*.md" ! -name "_template.md" | while read -r story; do
  EPIC=$(python3 -c "
import re
m = re.search(r'^epic:\s*[\"\'](.*?)[\"\']', open('$story').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
  SLUG=$(basename "$story" | sed 's/-[0-9]\{14\}-[0-9]\{4\}\.md$//')
  FEATURE_FILE="tests/features/$EPIC/$SLUG.feature"
  # extract-and-write step (see above) ...
  echo "[cucumber] SYNC  $story → $FEATURE_FILE"
done

Playwright + behave Integration

When the stack includes Playwright (requirements.txt contains playwright or behave), step definitions must follow these patterns. Read the QA agent's BDD section for the full antipatterns list.

environment.py template

import json
import threading
from playwright.sync_api import sync_playwright

BASE_URL = "http://localhost:3000"
DEFAULT_GEO = {"latitude": 40.7128, "longitude": -74.0060}

MOCK_FORECAST = {
    "daily": {
        "time": ["2025-01-01","2025-01-02","2025-01-03",
                 "2025-01-04","2025-01-05","2025-01-06","2025-01-07"],
        "temperature_2m_max": [22.1, 18.3, 15.0, 20.5, 25.2, 19.8, 17.4],
        "temperature_2m_min": [12.0, 10.5,  9.3, 13.2, 16.1, 11.4, 10.9],
        "weathercode":        [0,    3,     61,   0,    1,    80,   71 ],
        "precipitation_probability_max": [5, 30, 85, 10, 20, 70, 60],
        "windspeed_10m_max":  [12.0, 20.5, 35.2, 8.4, 15.6, 28.9, 22.3],
        "relative_humidity_2m_max": [55, 72, 90, 48, 62, 85, 78],
    }
}

def before_all(context):
    context.playwright = sync_playwright().start()
    context.browser = context.playwright.chromium.launch()

def after_all(context):
    context.browser.close()
    context.playwright.stop()

def before_scenario(context, scenario):
    context.browser_context = context.browser.new_context(
        permissions=["geolocation"],
        geolocation=DEFAULT_GEO,
    )
    context.page = context.browser_context.new_page()
    context._forecast_event = None

def after_scenario(context, scenario):
    context.page.close()
    context.browser_context.close()

# ── Helpers ────────────────────────────────────────────────────────────────────

def setup_forecast_route(page, error=False, delay_event=None):
    """Network-level Open-Meteo intercept. NEVER overrides window.fetch."""
    def handle(route):
        if delay_event is not None:
            delay_event.wait(timeout=30)
        if error:
            route.fulfill(status=500, content_type="application/json",
                          body='{"error":"service unavailable"}')
        else:
            route.fulfill(status=200, content_type="application/json",
                          body=json.dumps(MOCK_FORECAST))
    page.route("**/api.open-meteo.com/**", handle)

def navigate_and_wait(page):
    page.goto(BASE_URL, wait_until="domcontentloaded")
    page.wait_for_selector(".forecast-card", timeout=10_000)

Browser capability scenarios

# Geolocation granted — default, handled by before_scenario

# Geolocation denied — create context without the permission
context.browser_context = context.browser.new_context()  # no geolocation permission

# Geolocation unsupported — NO Playwright native API; add_init_script is acceptable HERE ONLY
context.page.add_init_script(
    "Object.defineProperty(navigator,'geolocation',{value:undefined,configurable:true});"
)

# Geolocation timeout — NO Playwright native API; add_init_script acceptable HERE ONLY
context.page.add_init_script(
    "navigator.geolocation.getCurrentPosition = function(){};"  # never calls back
)

Loading state (route with threading.Event)

# Setup step — route stays blocked until event is set
event = threading.Event()
setup_forecast_route(context.page, delay_event=event)
context._forecast_event = event

# Assert spinner visible while blocked
spinner = context.page.locator('[role="status"]')
spinner.wait_for(state="visible", timeout=3000)

# Unblock route → cards should appear
context._forecast_event.set()
context.page.wait_for_selector(".forecast-card", timeout=5000)

API error state

setup_forecast_route(context.page, error=True)
# route returns HTTP 500 — app must handle gracefully

Antipatterns checklist (rubber duck will flag these)

Antipattern Why it's wrong
add_init_script("window.fetch = ...") Bypasses all app network code; test passes regardless of implementation
page.evaluate("window._appResolve()") Reaches into app internals; not observable behaviour
add_init_script(GEO_SUCCESS_SCRIPT) for geolocation Playwright's context API exists — use it
page.evaluate("window.__mock = ...") Same as window.fetch override — leaks test state into app
Assertions checking internal state (data-* set by test, not app) Test the rendered output, not the test's own injected data

Failure Modes

Condition Action
Story file has no gherkin fenced block Log NO_GHERKIN. Skip. Do not create empty feature file.
Feature file already exists with manual edits Compare checksums. If changed: log MANUAL_EDIT — skip overwrite. Never overwrite manual edits.
Step file already exists Skip generation. Log SKIP. Agents implement stubs; generator does not regenerate.
Stack is unknown Log warning. Write .feature file only; skip step stubs.
Gherkin parse error (malformed block) Log the offending line. Skip the file. Do not attempt partial extraction.

Logging

[cucumber] WRITE   tests/features/user-auth/reset-password.feature  scenarios=3
[cucumber] STUBS   tests/step-definitions/user-auth/reset-password.steps.ts  steps=7
[cucumber] SKIP    tests/step-definitions/user-auth/login.steps.ts  (already exists)
[cucumber] NO_GHERKIN  docs/stories/auth-spike-20250416143000-0002.md  (spike — expected)
读取和写入W3C DTCG格式的设计令牌(tokens.json),并生成CSS自定义属性、Tailwind配置及Mantine主题对象。tokens.json为唯一权威来源,其他输出均由其自动派生。
修改设计令牌 生成CSS或Tailwind样式 更新Mantine主题
.claude/skills/design-tokens/SKILL.md
npx skills add kinncj/Heimdall --skill design-tokens -g -y
SKILL.md
Frontmatter
{
    "name": "design-tokens",
    "description": "Read and write design tokens in W3C DTCG format (tokens.json) and emit CSS, Tailwind, and Mantine outputs. Use when modifying or generating design tokens."
}

SKILL: design-tokens

Purpose

Read and write design tokens in W3C Design Token Community Group (DTCG) format (tokens.json). Emit framework-specific outputs: CSS custom properties, Tailwind config, and Mantine theme object. The tokens.json in docs/design/identity/ is the canonical source of truth; all framework outputs are derived and regenerated — never manually edited.

Token File: docs/design/identity/tokens.json

W3C DTCG format. Each token is an object with $value and $type:

{
  "$schema": "https://design-tokens.org/schema/v1.0.0",
  "_meta": { "status": "draft", "version": "1.0.0" },

  "color": {
    "brand": {
      "primary":   { "$value": "#2563eb", "$type": "color" },
      "secondary": { "$value": "#7c3aed", "$type": "color" },
      "accent":    { "$value": "#0ea5e9", "$type": "color" }
    },
    "semantic": {
      "success": { "$value": "#16a34a", "$type": "color" },
      "warning": { "$value": "#d97706", "$type": "color" },
      "error":   { "$value": "#dc2626", "$type": "color" },
      "info":    { "$value": "#0284c7", "$type": "color" }
    },
    "neutral": {
      "50":  { "$value": "#f8fafc", "$type": "color" },
      "500": { "$value": "#64748b", "$type": "color" },
      "900": { "$value": "#0f172a", "$type": "color" }
    },
    "surface": {
      "background": { "$value": "#ffffff", "$type": "color" },
      "foreground": { "$value": "#0f172a", "$type": "color" },
      "muted":      { "$value": "#f1f5f9", "$type": "color" },
      "border":     { "$value": "#e2e8f0", "$type": "color" }
    }
  },

  "typography": {
    "fontFamily": {
      "sans": { "$value": "Inter, system-ui, sans-serif", "$type": "fontFamily" },
      "mono": { "$value": "JetBrains Mono, monospace",   "$type": "fontFamily" }
    },
    "fontSize": {
      "sm":   { "$value": "0.875rem", "$type": "dimension" },
      "base": { "$value": "1rem",     "$type": "dimension" },
      "lg":   { "$value": "1.125rem", "$type": "dimension" },
      "xl":   { "$value": "1.25rem",  "$type": "dimension" },
      "2xl":  { "$value": "1.5rem",   "$type": "dimension" },
      "4xl":  { "$value": "2.25rem",  "$type": "dimension" }
    }
  },

  "spacing": {
    "1": { "$value": "0.25rem", "$type": "dimension" },
    "2": { "$value": "0.5rem",  "$type": "dimension" },
    "4": { "$value": "1rem",    "$type": "dimension" },
    "8": { "$value": "2rem",    "$type": "dimension" }
  },

  "radius": {
    "sm":   { "$value": "0.125rem", "$type": "dimension" },
    "md":   { "$value": "0.375rem", "$type": "dimension" },
    "lg":   { "$value": "0.5rem",   "$type": "dimension" },
    "full": { "$value": "9999px",   "$type": "dimension" }
  },

  "shadow": {
    "sm": { "$value": "0 1px 2px 0 rgb(0 0 0 / 0.05)", "$type": "shadow" },
    "md": { "$value": "0 4px 6px -1px rgb(0 0 0 / 0.1)", "$type": "shadow" },
    "lg": { "$value": "0 10px 15px -3px rgb(0 0 0 / 0.1)", "$type": "shadow" }
  }
}

Emit: CSS Custom Properties

Output to docs/design/identity/tokens.css:

import json, re

def flatten(obj, prefix=""):
    result = {}
    for k, v in obj.items():
        if k.startswith("$") or k.startswith("_"):
            continue
        key = f"{prefix}-{k}" if prefix else k
        if isinstance(v, dict) and "$value" in v:
            result[key] = v["$value"]
        elif isinstance(v, dict):
            result.update(flatten(v, key))
    return result

tokens = json.load(open("docs/design/identity/tokens.json"))
flat = flatten(tokens)

lines = [":root {"]
for name, value in sorted(flat.items()):
    css_var = "--" + name.replace(".", "-")
    lines.append(f"  {css_var}: {value};")
lines.append("}")

with open("docs/design/identity/tokens.css", "w") as f:
    f.write("\n".join(lines) + "\n")

print(f"[design-tokens] CSS  docs/design/identity/tokens.css  vars={len(flat)}")

Emit: Tailwind Config

Output to docs/design/identity/tailwind.tokens.js:

import json

tokens = json.load(open("docs/design/identity/tokens.json"))

def extract_colors():
    result = {}
    for group, values in tokens.get("color", {}).items():
        result[group] = {}
        for name, token in values.items():
            if "$value" in token:
                result[group][name] = token["$value"]
    return result

colors = extract_colors()
font_family = {
    k: v["$value"]
    for k, v in tokens.get("typography", {}).get("fontFamily", {}).items()
}

config = f"""/** @type {{import('tailwindcss').Config}} */
// AUTO-GENERATED — edit docs/design/identity/tokens.json, not this file
module.exports = {{
  theme: {{
    extend: {{
      colors: {json.dumps(colors, indent(6))},
      fontFamily: {json.dumps(font_family, indent(6))},
    }},
  }},
}};
"""

with open("docs/design/identity/tailwind.tokens.js", "w") as f:
    f.write(config)

print("[design-tokens] TAILWIND  docs/design/identity/tailwind.tokens.js")

Emit: Mantine Theme

Output to docs/design/identity/mantine.theme.ts:

import json

tokens = json.load(open("docs/design/identity/tokens.json"))

primary = tokens["color"]["brand"]["primary"]["$value"]
secondary = tokens["color"]["brand"]["secondary"]["$value"]

theme = f"""// AUTO-GENERATED — edit docs/design/identity/tokens.json, not this file
import {{ createTheme }} from '@mantine/core';

export const theme = createTheme({{
  primaryColor: 'brand',
  colors: {{
    brand: [
      '{tokens["color"]["neutral"]["50"]["$value"]}',
      '{tokens["color"]["neutral"]["100"]["$value"] if "100" in tokens["color"]["neutral"] else "#f1f5f9"}',
      '{tokens["color"]["neutral"]["200"]["$value"] if "200" in tokens["color"]["neutral"] else "#e2e8f0"}',
      '{tokens["color"]["neutral"]["300"]["$value"] if "300" in tokens["color"]["neutral"] else "#cbd5e1"}',
      '{tokens["color"]["neutral"]["400"]["$value"] if "400" in tokens["color"]["neutral"] else "#94a3b8"}',
      '{tokens["color"]["neutral"]["500"]["$value"]}',
      '{primary}',
      '{tokens["color"]["neutral"]["700"]["$value"] if "700" in tokens["color"]["neutral"] else "#334155"}',
      '{tokens["color"]["neutral"]["800"]["$value"] if "800" in tokens["color"]["neutral"] else "#1e293b"}',
      '{tokens["color"]["neutral"]["900"]["$value"]}',
    ],
  }},
  fontFamily: '{tokens["typography"]["fontFamily"]["sans"]["$value"]}',
  fontFamilyMonospace: '{tokens["typography"]["fontFamily"]["mono"]["$value"]}',
}});
"""

with open("docs/design/identity/mantine.theme.ts", "w") as f:
    f.write(theme)

print("[design-tokens] MANTINE  docs/design/identity/mantine.theme.ts")

Emit: Terminal Theme (tui target)

When design.target is tui, emit docs/design/identity/terminal-theme.json from tokens.json instead of the CSS/Tailwind/Mantine outputs. Map each color role to an ANSI-256 index (and the hex) plus a lipgloss style name, and record the foreground/background pairs with their contrast ratios so the a11y auditor can flag any pair under WCAG 2.2 AA. Shape:

{
  "roles": {
    "primary":    { "fg": "#7aa2f7", "ansi256": 111, "lipgloss": "primary" },
    "muted":      { "fg": "#565f89", "ansi256": 60,  "lipgloss": "muted" },
    "accent":     { "fg": "#bb9af7", "ansi256": 141, "lipgloss": "accent" },
    "error":      { "fg": "#f7768e", "ansi256": 204, "lipgloss": "error" },
    "success":    { "fg": "#9ece6a", "ansi256": 149, "lipgloss": "success" },
    "background": { "bg": "#1a1b26", "ansi256": 234 },
    "foreground": { "fg": "#c0caf5", "ansi256": 189 }
  },
  "pairs": [
    { "role": "foreground", "on": "background", "contrast": 12.6 },
    { "role": "muted",      "on": "background", "contrast": 3.4 }
  ]
}

Run All Emitters

python3 - <<'EOF'
# Run all three emitters in sequence
# (inline the three scripts above, or import from scripts/emit-tokens.py)
EOF

Update Tokens (read-write)

To update a single token value:

import json, sys

tokens = json.load(open("docs/design/identity/tokens.json"))
# Set tokens["color"]["brand"]["primary"]["$value"] = "#1d4ed8"
# Write back, then re-run all emitters
json.dump(tokens, open("docs/design/identity/tokens.json", "w"), indent=2)

Always re-emit all framework outputs after any token change.

Failure Modes

Condition Action
tokens.json malformed Log parse error with line number. Do not emit partial output.
Missing required token key Log MISSING_TOKEN: {path}. Emit with placeholder TODO value.
Emitted file differs from committed Acceptable — these are always regenerated.
_meta.status != approved Log warning. Emit anyway (dev workflow). Gate is in visual-identity approval, not here.

Logging

[design-tokens] READ    docs/design/identity/tokens.json  tokens=47
[design-tokens] CSS     docs/design/identity/tokens.css   vars=47
[design-tokens] TAILWIND docs/design/identity/tailwind.tokens.js
[design-tokens] MANTINE  docs/design/identity/mantine.theme.ts
[design-tokens] MISSING_TOKEN  color.neutral.100  — using placeholder
提供Docker和Docker Compose最佳实践,包括多阶段构建、健康检查配置及.dockerignore规则。适用于编写或审查Dockerfile与Compose文件,确保安全性、高效性及容器化服务的可靠性。
编写Dockerfile 创建docker-compose配置文件 审查容器化部署配置 优化镜像构建流程
.claude/skills/docker-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill docker-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "docker-patterns",
    "description": "Apply Docker and Docker Compose best practices for containerising services. Use when writing or reviewing Dockerfiles and compose configs."
}

SKILL: Docker Patterns

Multi-Stage Dockerfile Pattern

# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app

FROM base AS deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

FROM base AS dev-deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

FROM base AS build
COPY --from=dev-deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs22-debian12 AS production
WORKDIR /app
USER 1001
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["dist/main.js"]

docker-compose Health Check Pattern

services:
  app:
    build:
      context: .
      target: production
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 5s
      retries: 10

.dockerignore

node_modules
.git
.github
dist
build
coverage
.next
*.log
.env*
!.env.example
README.md
docs
tests

Key Rules

  • ALWAYS use multi-stage builds for production images.
  • ALWAYS run as non-root user (USER 1001 or named user).
  • ALWAYS add health checks to all services.
  • NEVER copy .env files into images.
  • Use --mount=type=cache for package manager caches.
  • Use distroless or alpine base images.
  • Use docker compose up -d --wait to wait for health checks.

Verification

# Build test
docker build --target production -t test-image .

# Security scan
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image test-image

# Start and verify health
docker compose up -d --wait
docker compose ps
用于在架构决策和基础设施变更审查中识别并标注云成本影响。涵盖计算、存储、网络及第三方API等驱动因素,提供Terraform注释格式、架构审查问题清单及月度成本估算模板,助力优化云支出。
审查基础设施变更 设计新服务时评估成本 进行架构决策分析
.claude/skills/finops-review/SKILL.md
npx skills add kinncj/Heimdall --skill finops-review -g -y
SKILL.md
Frontmatter
{
    "name": "finops-review",
    "description": "Identify and annotate cloud cost implications of architectural decisions. Use when reviewing infrastructure changes or designing new services."
}

SKILL: FinOps Review

Purpose

Identify and annotate the cloud cost implications of architectural decisions.

Cost Driver Categories

Compute

  • Lambda/Functions: invocation count x duration x memory
  • EC2/VMs: instance type x hours + data transfer
  • Containers: CPU/memory reservation x hours
  • Edge functions: request count x execution time

Storage

  • Object storage (S3/GCS/Blob): GB stored + requests + egress
  • Database: instance size + storage + IOPS + backups
  • Cache: node size x hours

Networking

  • Egress: GB transferred out (most expensive)
  • CDN: requests + GB served
  • Load balancer: hours + LCU

Third-party APIs

  • Stripe: 2.9% + $0.30 per transaction
  • Supabase: row reads/writes, storage, Edge Function invocations
  • Vercel: bandwidth, function invocations, seats

Annotation Format (Terraform)

resource "aws_db_instance" "main" {
  # finops: ~$180/mo (db.t3.medium, 100GB gp3, 1 AZ)
  # finops: scale-trigger: >70% CPU or >80% storage
  instance_class = "db.t3.medium"
  ...
}

Architecture Review Questions

  1. What scales with user count? What scales with data volume?
  2. Are there N+1 API calls that will cost $$ at scale?
  3. Is egress minimized? (Cache aggressively, colocate services)
  4. Are reserved instances/savings plans applicable?
  5. What is the cost at 10x current load?

Monthly Estimate Template

## Cost Estimate: {Feature}

| Service | Config | Est. Monthly |
|---------|--------|-------------|
| RDS PostgreSQL | db.t3.medium, 100GB | $180 |
| ElastiCache Redis | cache.t3.micro | $25 |
| ECS Fargate | 2 tasks, 0.5 vCPU, 1GB | $30 |
| ALB | 1 ALB, ~1M requests | $20 |
| **Total** | | **~$255/mo** |

Scales linearly with: [traffic volume / data volume]
自动化管理GitHub Issue全生命周期,支持创建、查看、编辑标签与指派、评论及关闭。通过CLI操作关联故事制品,自动记录阶段状态与TDD结果,实现无干预的故事流转管理。
需要创建或更新GitHub Issue以追踪故事进展时 在故事不同阶段(如发现、架构、实现)需要标记状态并通知相关人员时 遇到测试失败或阻塞情况需要记录日志并请求人工介入时
.claude/skills/gh-issues/SKILL.md
npx skills add kinncj/Heimdall --skill gh-issues -g -y
SKILL.md
Frontmatter
{
    "name": "gh-issues",
    "description": "Create, read, update, and link GitHub Issues as story artifacts throughout the story lifecycle. Use when managing issues for stories."
}

SKILL: gh-issues

Purpose

Create, read, update, and link GitHub Issues as first-class story artifacts. Agents use this skill to manage issue lifecycle — from PO story creation through QA closure — without human intervention.

Inputs

Field Source Example
title story frontmatter "User can reset password"
body_file story file path docs/stories/auth-reset-0001.md
labels story frontmatter + phase "type:feature,priority:high,phase:discover"
milestone project.config.yaml "v1.0"
assignee orchestrator context "@me"
issue_number previously created issue 42

Outputs

Field Description
issue_number integer, use for all subsequent operations
issue_url full GitHub URL for logging
issue_node_id GraphQL node ID, needed for Projects v2 card add

Create an Issue

# Capture issue number and URL
RESULT=$(gh issue create \
  --title "{title}" \
  --body-file "{body_file}" \
  --label "{labels}" \
  --milestone "{milestone}" \
  --json number,url,id)

ISSUE_NUMBER=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['number'])")
ISSUE_URL=$(echo "$RESULT"    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['url'])")
ISSUE_NODE=$(echo "$RESULT"   | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])")

View an Issue

gh issue view {issue_number} --json number,title,state,labels,body

Edit Labels and Assignee

# Add label, remove old phase label
gh issue edit {issue_number} \
  --add-label "phase:architect" \
  --remove-label "phase:discover"

# Assign to current actor
gh issue edit {issue_number} --add-assignee "@me"

Add a Comment

gh issue comment {issue_number} \
  --body "{message}"

Comment conventions by role:

Event Template
Phase start "Phase {N} {PHASE_NAME}: starting. Artifact target: {path}"
Phase complete "Phase {N} {PHASE_NAME}: complete. Artifacts: {path}"
TDD red "RED: Failing test at {test_path} — {failure_message}"
TDD green "GREEN: {test_count} tests passing."
Blocked "BLOCKED: {agent} failed 3x on {task}. Human intervention required."

Close an Issue

# Close with final validation summary
gh issue close {issue_number} \
  --comment "All acceptance criteria passing. Validation: {summary}"

List Issues

# All open issues for current phase
gh issue list --label "phase:implement" --state open --json number,title,labels

# Blocked issues
gh issue list --label "blocked" --state open

Link Issues (parent / blocks)

GitHub Issues has no native parent field — use body references and labels:

# In the child issue body, add:
# "Closes #{parent_number}" → auto-closes parent when child closes
# "Part of #{epic_issue_number}"

# For blocking relationships, comment on the blocked issue:
gh issue comment {blocked_number} \
  --body "Blocked by #{blocker_number} — {reason}"
gh issue edit {blocked_number} --add-label "blocked"

Failure Modes

Condition Action
gh not authenticated Stop. Output: gh auth login required. Do not retry.
Issue already exists with same title Check with gh issue list --search "{title}" --state all. Skip create if found; return existing number.
Label does not exist Create label first via gh label create. See gh-labels-milestones skill.
Milestone not found Create milestone first: gh api repos/{owner}/{repo}/milestones -f title="{name}".
Rate limit hit Wait 60 seconds, retry once. If second attempt fails, stop and log.

Logging

Always log after every gh issue mutation:

[gh-issues] CREATE  #42  "User can reset password"  labels=[type:feature,priority:high]
[gh-issues] COMMENT #42  phase:implement start
[gh-issues] CLOSE   #42  all criteria passing

Format: [gh-issues] {ACTION} #{number} {description}

幂等同步GitHub仓库的Labels和Milestones。用于新项目初始化或CI中确保标签状态一致,支持按阶段、类型、优先级等分组创建或更新标签,绝不删除。
初始化新GitHub仓库 CI流水线中同步标签定义 项目启动时设置标准标签
.claude/skills/gh-labels-milestones/SKILL.md
npx skills add kinncj/Heimdall --skill gh-labels-milestones -g -y
SKILL.md
Frontmatter
{
    "name": "gh-labels-milestones",
    "description": "Bootstrap and sync GitHub labels and milestones idempotently. Use when setting up a new repository or syncing label definitions."
}

SKILL: gh-labels-milestones

Purpose

Bootstrap and sync GitHub labels and milestones idempotently. Labels and milestones are created if absent; existing ones are updated if color or description differs. Never delete — only add or update.

Label Bootstrap

Use this at project start (maple labels) and in CI to guarantee label state.

# Idempotent label create-or-update
upsert_label() {
  local name="$1" color="$2" description="$3"
  if gh label list --json name --jq '.[].name' | grep -qx "$name"; then
    gh label edit "$name" --color "$color" --description "$description"
  else
    gh label create "$name" --color "$color" --description "$description"
  fi
}

Label Groups

Phase Labels (pipeline position)

upsert_label "phase:discover"   "0075ca" "Phase 1: Discovery"
upsert_label "phase:architect"  "0075ca" "Phase 2: Architecture"
upsert_label "phase:plan"       "0075ca" "Phase 3: Planning"
upsert_label "phase:infra"      "0075ca" "Phase 4: Infrastructure"
upsert_label "phase:implement"  "0075ca" "Phase 5: Implementation"
upsert_label "phase:validate"   "0075ca" "Phase 6: Validation"
upsert_label "phase:document"   "0075ca" "Phase 7: Documentation"
upsert_label "phase:done"       "0075ca" "Phase 8: Complete"

Type Labels (work classification)

upsert_label "type:feature"     "0052cc" "New capability"
upsert_label "type:bug"         "d73a4a" "Defect"
upsert_label "type:spike"       "e4e669" "Research / time-boxed exploration"
upsert_label "type:chore"       "ededed" "Non-functional maintenance"
upsert_label "type:refactor"    "ededed" "Code restructuring, no behavior change"
upsert_label "type:docs"        "0075ca" "Documentation only"

Priority Labels (MoSCoW)

upsert_label "priority:critical" "b60205" "Must ship — blocks launch"
upsert_label "priority:high"     "e11d48" "Must have"
upsert_label "priority:medium"   "f97316" "Should have"
upsert_label "priority:low"      "84cc16" "Could have"
upsert_label "priority:wontfix"  "ffffff" "Won't have this cycle"

Spec / Story Kit Labels

upsert_label "spec:problem"     "7057ff" "Problem statement written"
upsert_label "spec:approved"    "7057ff" "Spec approved by PO"
upsert_label "spec:in-review"   "7057ff" "Spec under review"

Design Labels

upsert_label "design:pending"      "fbca04" "Awaiting design"
upsert_label "design:in-progress"  "fbca04" "Design work active"
upsert_label "design:approved"     "fbca04" "Design approved"
upsert_label "design:a11y-passed"  "fbca04" "Accessibility review passed"

ADR Labels

upsert_label "adr:required"    "5319e7" "ADR must be written before implementation"
upsert_label "adr:in-progress" "5319e7" "ADR being authored"
upsert_label "adr:complete"    "5319e7" "ADR accepted"
upsert_label "adr:rejected"    "5319e7" "ADR rejected — see comments"

UI / Accessibility Labels

upsert_label "ui:required"     "fef2c0" "Has UI surface — design review required"
upsert_label "ui:in-progress"  "fef2c0" "UI implementation active"
upsert_label "ui:complete"     "fef2c0" "UI complete and reviewed"

Status Labels

upsert_label "in-progress" "0052cc" "Work started"
upsert_label "blocked"     "b60205" "Blocked — needs human"
upsert_label "validated"   "0e8a16" "All tests pass"
upsert_label "tdd:red"     "d73a4a" "Failing test written"
upsert_label "tdd:green"   "0e8a16" "Tests passing"

Milestone Bootstrap

# Idempotent milestone create
upsert_milestone() {
  local title="$1" due="$2" description="$3"
  EXISTING=$(gh api "repos/{owner}/{repo}/milestones" \
    --jq ".[] | select(.title == \"$title\") | .number" 2>/dev/null)
  if [ -n "$EXISTING" ]; then
    gh api "repos/{owner}/{repo}/milestones/$EXISTING" \
      -X PATCH \
      -f title="$title" \
      -f due_on="${due}T00:00:00Z" \
      -f description="$description"
  else
    gh api "repos/{owner}/{repo}/milestones" \
      -X POST \
      -f title="$title" \
      -f due_on="${due}T00:00:00Z" \
      -f description="$description"
  fi
}

# Example usage
upsert_milestone "v1.0" "2025-12-31" "Initial release"

Assign Labels to an Issue

# Add labels (idempotent — gh ignores if already present)
gh issue edit {issue_number} \
  --add-label "type:feature,priority:high,phase:discover"

# Remove a specific label
gh issue edit {issue_number} --remove-label "phase:discover"

Failure Modes

Condition Action
Label name collision (wrong color) gh label edit — update in place
Milestone already exists Patch via API — do not create duplicate
gh not authenticated Stop immediately. Do not retry.
Label with special characters URL-encode the label name in API calls

Logging

[gh-labels] UPSERT  label="type:feature"    color=0052cc
[gh-labels] SKIP    label="priority:high"   (unchanged)
[gh-milestones] CREATE  "v1.0"  due=2025-12-31
[gh-milestones] SKIP    "v1.0"  already exists
管理GitHub Projects v2看板,支持添加Issue卡片、更新字段值及查询状态。优先使用gh CLI,缺失时回退至GraphQL API。
需要向GitHub项目看板添加Issue 需要更新项目看板的字段值(如状态、类型)
.claude/skills/gh-projects/SKILL.md
npx skills add kinncj/Heimdall --skill gh-projects -g -y
SKILL.md
Frontmatter
{
    "name": "gh-projects",
    "description": "Manage GitHub Projects v2 board: add issues, update field values, and query board state. Use when managing project boards."
}

SKILL: gh-projects

Purpose

Manage GitHub Projects v2 board operations: add issues as cards, update field values (status, type, ADR required), and query board state. Uses gh project CLI where available; falls back to gh api graphql for operations the CLI does not expose.

Inputs

Field Source Example
project_number project.config.yaml 3
project_node_id project.config.yaml "PVT_kwDO..."
issue_node_id gh-issues skill output "I_kwDO..."
issue_number gh-issues skill output 42
field_name project field name "Status"
field_value target option name "In progress"

Read project config

PROJECT_NUMBER=$(python3 -c "
import sys
for line in open('project.config.yaml'):
    if 'project_number:' in line:
        print(line.split(':')[1].strip())
        break
")

PROJECT_NODE=$(python3 -c "
import sys
for line in open('project.config.yaml'):
    if 'project_node_id:' in line:
        print(line.split(':', 1)[1].strip())
        break
")

Add an Issue to the Project Board

# CLI (preferred)
gh project item-add "$PROJECT_NUMBER" \
  --owner "{owner}" \
  --url "https://github.com/{owner}/{repo}/issues/{issue_number}"

# GraphQL fallback (when CLI unavailable or for automation)
gh api graphql -f query='
  mutation($project: ID!, $content: ID!) {
    addProjectV2ItemById(input: {projectId: $project, contentId: $content}) {
      item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f content="{issue_node_id}" \
  --jq '.data.addProjectV2ItemById.item.id'

Save the returned item_id — required for field updates.

Get Field and Option IDs

Field updates require the field's node ID and the option's node ID (for single-select fields).

FIELDS=$(gh api graphql -f query='
  query($project: ID!) {
    node(id: $project) {
      ... on ProjectV2 {
        fields(first: 20) {
          nodes {
            ... on ProjectV2Field { id name }
            ... on ProjectV2SingleSelectField {
              id name
              options { id name }
            }
          }
        }
      }
    }
  }' \
  -f project="$PROJECT_NODE")

# Extract field ID by name
FIELD_ID=$(echo "$FIELDS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for f in d['data']['node']['fields']['nodes']:
    if f.get('name') == '{field_name}':
        print(f['id'])
        break
")

# Extract option ID by value (single-select fields)
OPTION_ID=$(echo "$FIELDS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for f in d['data']['node']['fields']['nodes']:
    if f.get('name') == '{field_name}':
        for opt in f.get('options', []):
            if opt['name'] == '{field_value}':
                print(opt['id'])
                break
")

Update a Single-Select Field (Status, Type, ADR Required)

gh api graphql -f query='
  mutation($project: ID!, $item: ID!, $field: ID!, $option: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $project
      itemId: $item
      fieldId: $field
      value: { singleSelectOptionId: $option }
    }) {
      projectV2Item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f item="{item_id}" \
  -f field="$FIELD_ID" \
  -f option="$OPTION_ID"

Update a Text Field (Epic, Specialist)

gh api graphql -f query='
  mutation($project: ID!, $item: ID!, $field: ID!, $value: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $project
      itemId: $item
      fieldId: $field
      value: { text: $value }
    }) {
      projectV2Item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f item="{item_id}" \
  -f field="$FIELD_ID" \
  -f value="{text_value}"

Standard Field Updates by Pipeline Phase

Phase Field Value
Discover Status Todo
Architect Status In progress
Implement Status In progress
Validate Status In Review
Done Status Done

Query Board State

# List all items with status
gh project item-list "$PROJECT_NUMBER" \
  --owner "{owner}" \
  --format json \
  --jq '.items[] | {id, title: .content.title, status: .fieldValues[] | select(.field.name=="Status") | .name}'

Failure Modes

Condition Action
project_node_id missing from config Run maple project to bootstrap. Stop until resolved.
Item already on board Query before adding. gh project item-list and check content URL. Skip if present.
Field ID not found Re-fetch fields. Field names are case-sensitive.
Option ID not found List available options from field metadata. Do not guess.
GraphQL rate limit Wait 60 seconds. Retry once. Log and stop on second failure.

Logging

[gh-projects] ADD    #42  → project #{project_number}  item_id={id}
[gh-projects] UPDATE #42  field=Status  value="In progress"
[gh-projects] SKIP   #42  already on board
生成符合规范的Gherkin故事文件,处理ID分配、时间戳、文件名及标签。提供模板与验证规则,确保场景单一职责、步骤声明式且格式正确,适用于产品负责人编写或更新故事文档。
编写新的用户故事文件 更新现有的Gherkin故事内容 需要为史诗分配新故事ID时
.claude/skills/gherkin-authoring/SKILL.md
npx skills add kinncj/Heimdall --skill gherkin-authoring -g -y
SKILL.md
Frontmatter
{
    "name": "gherkin-authoring",
    "description": "Produce valid, consistently named Gherkin story files with correct IDs, timestamps, and tags. Use when writing or updating story files."
}

SKILL: gherkin-authoring

Purpose

Produce valid, consistently named Gherkin story files. Handles ID allocation, timestamp generation, filename construction, tag formatting, and scenario validation. Used by the product-owner agent whenever a new story is written or updated.

Story File Naming Convention

<epic-slug>-<story-slug>-<timestamp>-<NNNN>.md
Part Format Example
epic-slug kebab-case, 2–4 words user-auth
story-slug kebab-case, 2–5 words reset-password
timestamp YYYYMMDDHHMMSS 20250416143000
NNNN 4-digit zero-padded, sequential within epic 0003

Full example: user-auth-reset-password-20250416143000-0003.md

Allocate the Next Story ID

# Count existing stories for an epic to get next sequential ID
EPIC="user-auth"
NEXT_ID=$(ls docs/stories/${EPIC}-* 2>/dev/null \
  | grep -oE '\-[0-9]{4}\.md$' \
  | grep -oE '[0-9]{4}' \
  | sort -n | tail -1 \
  | python3 -c "import sys; n=sys.stdin.read().strip(); print(f'{int(n)+1:04d}' if n else '0001')")
[ -z "$NEXT_ID" ] && NEXT_ID="0001"
echo "$NEXT_ID"

Generate Filename

EPIC="user-auth"
STORY="reset-password"
TS=$(date -u +"%Y%m%d%H%M%S")
ID="$NEXT_ID"   # from allocation step above
FILENAME="${EPIC}-${STORY}-${TS}-${ID}.md"
echo "$FILENAME"   # user-auth-reset-password-20250416143000-0001.md

Story File Template

Every story file must contain:

---
id: "{EPIC}-{NNNN}"
title: "{Human-readable story title}"
epic: "{epic-slug}"
priority: "high|medium|low|critical"
ui: false
adr_required: false
milestone: "{milestone name}"
labels:
  - "type:feature"
  - "priority:high"
  - "phase:discover"
issue_number: null
issue_url: null
---

## Story

**As a** {actor},
**I want** {capability},
**so that** {business outcome}.

## Acceptance Criteria

```gherkin
@story:{EPIC}-{NNNN} @epic:{epic-slug} @priority:{level}
Feature: {Feature title}

  Scenario: {scenario title}
    Given {precondition}
    When {action}
    Then {expected outcome}

Definition of Done

  • All Gherkin scenarios have passing step implementations
  • Unit tests written and passing
  • Integration tests written and passing (if applicable)
  • Code reviewed and approved
  • No regressions in related features
  • Documentation updated (if applicable)

ADR Links


## Gherkin Validation Rules

Before writing a scenario, verify:

1. **Single responsibility**: each `Scenario` tests exactly one behavior.
2. **Given-When-Then order**: never skip or reorder.
3. **No implementation detail in steps**: steps describe intent, not code.
4. **Declarative steps**: `Given the user is logged in` ✓ — `Given I call POST /auth/login` ✗
5. **Tags present**: `@story:`, `@epic:`, `@priority:` required on every Feature block.
6. **No `And` as first step**: `And` is only valid after a `Given`, `When`, or `Then`.

## Background Block (shared preconditions)

```gherkin
Feature: Password Reset

  Background:
    Given the user has a verified account
    And the account is not locked

  Scenario: Successful reset request
    When the user submits their email address
    Then a reset link is sent to that address

Use Background when 2+ scenarios share the same preconditions.

Scenario Outline (data-driven)

  Scenario Outline: Reject invalid email formats
    When the user submits "<email>"
    Then they see a validation error "<message>"

    Examples:
      | email        | message                  |
      | notanemail   | Invalid email format     |
      | @nodomain    | Invalid email format     |
      | a@b          | Domain must have TLD     |

Tags Reference

Tag Required Description
@story:{id} Yes Links scenario to story file
@epic:{slug} Yes Groups stories by epic
@priority:{level} Yes critical, high, medium, low
@wip No Work in progress — excluded from CI by default
@ui No Requires browser/UI driver
@integration No Requires external services
@smoke No Runs in production smoke suite

Failure Modes

Condition Action
Story ID collision (same NNNN exists) Re-run allocation step. Never overwrite an existing file.
docs/stories/ does not exist Create it: mkdir -p docs/stories/
Scenario has no When Invalid — split into two scenarios or add action step
Missing required tags Add tags before writing the file
Title exceeds 72 characters Shorten. Titles appear in issue headings and PR titles.

Logging

[gherkin] ALLOCATE  epic=user-auth  next_id=0003
[gherkin] CREATE    docs/stories/user-auth-reset-password-20250416143000-0003.md
[gherkin] VALIDATE  scenarios=3  tags=ok  structure=ok
GitHub CLI 综合参考,涵盖仓库上下文、Issue(创建/查询/更新/评论/关闭)及PR(草稿/审查/合并)操作。适用于所有 GitHub 交互场景。
需要创建或管理 GitHub Issue 需要处理 Pull Request 流程 查询仓库状态或分支信息 执行 GitHub Actions 相关搜索
.claude/skills/github-cli/SKILL.md
npx skills add kinncj/Heimdall --skill github-cli -g -y
SKILL.md
Frontmatter
{
    "name": "github-cli",
    "description": "Comprehensive gh CLI reference — issues, tasks, PRs, repos, branches, Actions, search, and project board. Use whenever interacting with GitHub."
}

SKILL: GitHub CLI

Repo Context (always run first)

# Who am I, what repo am I in
gh auth status
gh repo view --json name,owner,defaultBranchRef,url,isPrivate

# Open issues + PRs at a glance
gh issue list --state open --limit 20 --json number,title,labels,assignees
gh pr list --state open --json number,title,headRefName,statusCheckRollup

Issues as Tasks

Create

# Feature story
gh issue create \
  --title "feat: {title}" \
  --body-file docs/stories/{slug}/Story.md \
  --label "type:feature,priority:high,phase:discover" \
  --milestone "v1.0" \
  --assignee "@me"

# Bug
gh issue create \
  --title "fix: {title}" \
  --body "## Steps to reproduce\n{steps}\n\n## Expected\n{expected}\n\n## Actual\n{actual}" \
  --label "type:bug,priority:high"

# Task (sub-work, no story file)
gh issue create \
  --title "task: {title}" \
  --body "{description}" \
  --label "type:task" \
  --assignee "@me"

Read

gh issue view {number}
gh issue view {number} --json number,title,state,body,labels,assignees,comments,milestone

# Search
gh issue list --search "label:blocked" --state open
gh issue list --search "assignee:@me is:open"
gh issue list --search "phase:implement" --label "tdd:red"
gh issue list --state open --label "type:feature" --milestone "v1.0"

Update

# Phase transition
gh issue edit {number} \
  --add-label "phase:architect" \
  --remove-label "phase:discover"

# Assign / unassign
gh issue edit {number} --add-assignee "@me"
gh issue edit {number} --remove-assignee "{login}"

# Change milestone
gh issue edit {number} --milestone "v2.0"

# Block / unblock
gh issue edit {number} --add-label "blocked"
gh issue edit {number} --remove-label "blocked"

Comment

gh issue comment {number} --body "{message}"

# Phase lifecycle comments (standard format)
gh issue comment {number} --body "Phase {N} {NAME}: starting. Target artifacts: {paths}"
gh issue comment {number} --body "Phase {N} {NAME}: complete. Artifacts: {paths}"
gh issue comment {number} --body "RED: Failing test at {path} — {message}"
gh issue comment {number} --body "GREEN: {count} tests passing."
gh issue comment {number} --body "BLOCKED: {agent} failed 3x on {task}. Human required."

Close

gh issue close {number} --comment "Acceptance criteria passing. Validation: {summary}"
gh issue close {number} --reason "not planned"

Pull Requests

# Create PR (always draft first)
gh pr create \
  --title "feat: {description}" \
  --body "## Summary\n{summary}\n\nCloses #{issue_number}" \
  --base main \
  --draft

# Mark ready when all gates pass
gh pr ready {number}

# View PR + CI status
gh pr view {number} --json number,title,state,reviews,statusCheckRollup,files

# Request review
gh pr edit {number} --add-reviewer "{login}"

# Check CI on PR
gh pr checks {number}

# Merge (squash — preferred)
gh pr merge {number} --squash --subject "feat: {description} (#{issue_number})" --delete-branch

# Review
gh pr review {number} --approve --body "LGTM"
gh pr review {number} --request-changes --body "{feedback}"

Branches

# List branches
gh api repos/{owner}/{repo}/branches --jq '.[].name'

# Create via API (when not in git context)
gh api repos/{owner}/{repo}/git/refs \
  -f ref="refs/heads/feat/{slug}" \
  -f sha="$(gh api repos/{owner}/{repo}/git/ref/heads/main --jq '.object.sha')"

# Delete remote branch
gh api -X DELETE repos/{owner}/{repo}/git/refs/heads/feat/{slug}

# Branch protection status
gh api repos/{owner}/{repo}/branches/main/protection

Actions / CI

# List recent runs
gh run list --limit 10
gh run list --workflow ci.yml --branch main --limit 5

# Watch a run live
gh run watch {run-id}

# View failed run details
gh run view {run-id} --log-failed

# Trigger workflow manually
gh workflow run {workflow-file} --ref main

# Re-run failed jobs only
gh run rerun {run-id} --failed

# Download artifact
gh run download {run-id} --name {artifact-name} --dir ./artifacts

Repo Inspection

# Full repo metadata
gh repo view --json name,owner,description,topics,visibility,defaultBranchRef,\
licenseInfo,stargazerCount,forkCount,createdAt,updatedAt

# Collaborators
gh api repos/{owner}/{repo}/collaborators --jq '.[].login'

# Repo settings
gh api repos/{owner}/{repo} --jq '{private:.private,hasIssues:.has_issues,hasWiki:.has_wiki}'

# Topics
gh repo edit --add-topic "{topic}"

# Clone URL
gh repo view --json sshUrl,url

# Releases
gh release list --limit 5
gh release view {tag}

Labels & Milestones

# Bootstrap MAPLE labels (idempotent)
bash scripts/bootstrap-labels.sh

# List labels
gh label list

# Create label
gh label create "type:task" --color "0075ca" --description "Work item"

# Create milestone
gh api repos/{owner}/{repo}/milestones \
  -f title="v1.0" \
  -f description="First release" \
  -f due_on="2026-06-01T00:00:00Z"

# List milestones
gh api repos/{owner}/{repo}/milestones --jq '.[] | {number:.number,title:.title,open:.open_issues}'

Search

# Search code in repo
gh search code "{query}" --repo {owner}/{repo} --json path,url

# Search issues across GitHub
gh search issues "{query}" --repo {owner}/{repo} --json number,title,state

# Search PRs
gh search prs "{query}" --repo {owner}/{repo} --state open

Issue Lifecycle (MAPLE Standard)

Event Label added Label removed Comment
Story created phase:discover "Phase 1 DISCOVER: story created."
Architect done phase:architect phase:discover "Phase 2 ARCHITECT: design complete."
Plan done phase:plan phase:architect "Phase 3 PLAN: plan.md ready."
Infra done phase:infra phase:plan "Phase 4 INFRA: containers healthy."
Implement start phase:implement,in-progress phase:infra "Phase 5 IMPLEMENT: TDD loop starting."
TDD red tdd:red "RED: failing test at {path}"
TDD green tdd:green tdd:red "GREEN: {n} tests passing."
Validate pass phase:validate,validated phase:implement "Phase 6 VALIDATE: 100% pass."
Document done phase:document phase:validate "Phase 7 DOCUMENT: docs complete."
PR created ready-for-review phase:document "Phase 8: PR #{pr} created."
Blocked blocked "BLOCKED: {agent} failed 3x. Human needed."
Unblocked blocked "Unblocked: {resolution}"

Failure Modes

Condition Action
Not authenticated gh auth login — do not retry ops
Label missing Create via gh label create before using
Milestone missing Create via gh api milestones before using
Rate limited Wait 60s, retry once; if fails again, stop
Issue already exists gh issue list --search "{title}" --state all — return existing number
gh not found Stop. Output: "gh CLI not available. Install from https://cli.github.com"
用于消除文本中AI生成的痕迹和模式,使文档、注释、提交信息更自然。支持手动调用或集成MAPLE工作流,通过检测29种AI写作特征并进行多轮重写优化,提升内容的人类化程度。
生成文档后需要润色 提交代码前检查提交信息和注释 更新README或CHANGELOG 将AI辅助生成的文本调整为更自然的语气
.claude/skills/humanizer/SKILL.md
npx skills add kinncj/Heimdall --skill humanizer -g -y
SKILL.md
Frontmatter
{
    "name": "humanizer",
    "description": "Remove AI-isms and artificial language patterns from text. Makes documentation, comments, commit messages, and prose sound more natural and human. Based on Wikipedia's \"Signs of AI writing\" patterns."
}

humanizer skill

Polish text by removing detectable AI-generated writing patterns. Ideal for finalizing documentation, commit messages, comments, and any prose before merging.

When to use

  • After @docs generates feature documentation
  • Before committing code (check commit messages and comments)
  • Polishing README updates, CHANGELOG entries, or runbooks
  • Making AI-assisted prose sound more natural
  • Manual voice calibration for brand consistency

How to invoke

/humanizer

[paste your text here]

Or ask directly in chat:

Please humanize this text: [your text]

Voice calibration (optional)

Provide a sample of your own writing for the skill to match your style:

/humanizer

Here's a sample of my writing for voice matching:
[paste 2-3 paragraphs of your own writing]

Now humanize this text:
[paste AI text to humanize]

What it detects

The skill checks for 29 AI-writing patterns, including:

  • Significance inflation — "marking a pivotal moment in the evolution of..." → "was established in 1989"
  • Notability name-dropping — Lists of citations → specific examples with context
  • Superficial -ing phrases — "symbolizing, reflecting, showcasing" → active explanations
  • Promotional language — Corporate jargon → straightforward description
  • Overuse of 'indeed' — Filler words → tighter prose
  • Transition abuse — Excessive "Furthermore, In conclusion..." → natural flow
  • Hedging redundancy — "arguably, in some sense, it could be argued..." → clarity
  • Rare word stacking — Thesaurus abuse → common vocabulary
  • False expert mode — Generic expertise → grounded examples
  • Generalist fluff — "many fields, various domains..." → specific scope

Integration with MAPLE

  • Auto-call after Phase 7 (DOCUMENT) if humanize: true in feature story frontmatter
  • Manual: invoke at any phase before merge
  • Works across all harnesses (Claude Code, OpenCode, Cursor, Copilot CLI)

Output

Returns a humanized version of your text with:

  1. First pass: Remove identified AI patterns
  2. Final audit: "Obviously AI generated?" check
  3. Second rewrite: Catch lingering AI-isms

Further reading

提供Jupyter Notebook最佳实践,涵盖单元格标准化排序、使用Papermill实现参数化执行、通过nbformat编程创建笔记本、利用nbconvert导出格式及Git提交前的输出清理规范,确保代码可复现性与协作卫生。
处理.ipynb文件时 需要优化Notebook结构或流程时 进行Notebook版本控制或导出时
.claude/skills/jupyter-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill jupyter-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "jupyter-patterns",
    "description": "Apply best practices for Jupyter notebooks: cell ordering, reproducibility, parameterisation. Use when working with .ipynb files."
}

SKILL: Jupyter Notebook Patterns

Notebook Structure

Cells should follow this order:

  1. Setup — imports, configuration, constants
  2. Data Loading — load raw data with validation
  3. EDA — exploratory data analysis, distributions, correlations
  4. Preprocessing — cleaning, feature engineering
  5. Modeling — model training and evaluation
  6. Visualization — charts and figures
  7. Conclusions — findings summary, next steps

Parameterized Execution with Papermill

# In notebook cell tagged with "parameters":
# Click: View -> Cell Toolbar -> Tags -> add "parameters" tag

dataset = "data/train.csv"  # papermill will override this
output_dir = "outputs"
n_estimators = 100
random_state = 42
# Execute with papermill
papermill input.ipynb output.ipynb \
  -p dataset "data/test.csv" \
  -p n_estimators 200 \
  -p random_state 0

Programmatic Notebook Creation

import nbformat as nbf

nb = nbf.v4.new_notebook()
nb.cells = [
    nbf.v4.new_markdown_cell("# Analysis: {Title}"),
    nbf.v4.new_code_cell("import pandas as pd\nimport numpy as np"),
    nbf.v4.new_code_cell("df = pd.read_csv('data.csv')\ndf.head()"),
]

with open('analysis.ipynb', 'w') as f:
    nbf.write(nb, f)

Export

# To HTML (with outputs)
jupyter nbconvert --to html --execute notebook.ipynb

# To PDF
jupyter nbconvert --to pdf --execute notebook.ipynb

# Execute in place
jupyter nbconvert --to notebook --execute --inplace notebook.ipynb

Git Hygiene

# Clear outputs before commit
jupyter nbconvert --to notebook --ClearOutputPreprocessor.enabled=True \
  --inplace notebook.ipynb

# Or use nbstripout (installs as git filter)
pip install nbstripout
nbstripout --install

Rules

  • Restart kernel and run all cells before committing.
  • Clear all outputs before git commit.
  • Tag parameter cells for papermill.
  • Each notebook should be self-contained and reproducible.
  • Include random_state parameter for reproducibility.
  • Use relative paths for data files.
基于Karpathy四大原则审计代码变更,确保思考充分、简洁、精准及目标导向。自动在实现阶段后触发或手动调用,生成合规报告并控制流程推进。
Phase 5 IMPLEMENT完成后自动触发 用户通过/karpathy-audit或@karpathy-audit手动调用
.claude/skills/karpathy-audit/SKILL.md
npx skills add kinncj/Heimdall --skill karpathy-audit -g -y
SKILL.md
Frontmatter
{
    "name": "karpathy-audit",
    "description": "Audit code changes against Karpathy's 4 principles (Think Before Coding, Simplicity First, Surgical Changes, Goal-Driven Execution). Auto-called after Phase 5 IMPLEMENT; can also be invoked manually. Produces scored compliance report and blocks advancement if threshold not met."
}

karpathy-audit skill

Audit PRs and code changes against Andrej Karpathy's 4 principles for reducing LLM coding mistakes.

When to use

  • Auto-invoked after Phase 5 (IMPLEMENT) before advancing to Phase 6 (VALIDATE)
  • Manual invocation: /karpathy-audit or @karpathy-audit in chat
  • At any phase when you want to assess code quality against the principles

The 4 Principles

1. Think Before Coding

Don't assume. Don't hide confusion. Surface tradeoffs.

  • State assumptions explicitly. If uncertain, ask.
  • Present multiple interpretations—don't pick silently.
  • If a simpler approach exists, say so. Push back when warranted.
  • Stop when confused. Name what's unclear and ask.

2. Simplicity First

Minimum code that solves the problem. Nothing speculative.

  • No features beyond what was asked.
  • No abstractions for single-use code.
  • No "flexibility" or "configurability" that wasn't requested.
  • No error handling for impossible scenarios.
  • If 200 lines could be 50, rewrite it.

3. Surgical Changes

Touch only what you must. Clean up only your own mess.

  • Don't "improve" adjacent code, comments, or formatting.
  • Don't refactor things that aren't broken.
  • Match existing style, even if you'd do it differently.
  • Only remove imports/variables/functions that YOUR changes made unused.

4. Goal-Driven Execution

Define success criteria. Loop until verified.

  • Transform tasks into verifiable goals: tests before code.
  • State a brief plan with explicit success criteria.
  • Loop independently until verified.

How to invoke

Auto-call (Phase 5 → Phase 6 gate): Orchestrator automatically calls this skill after IMPLEMENT phase to audit the diff.

Manual call:

/karpathy-audit

or

@karpathy-audit

Audit output

The skill produces a compliance report written to .claude/state/karpathy-report.json:

{
  "phase": "IMPLEMENT",
  "timestamp": "2026-05-18T21:25:00Z",
  "spec_file": "docs/stories/user-auth/Story.md",
  "audit": {
    "think_before_coding": {
      "score": 85,
      "violations": ["Assumed DB schema without asking"],
      "evidence": ["Line 42: const User = model('User')"]
    },
    "simplicity_first": {
      "score": 92,
      "violations": [],
      "evidence": ["Reduced 180→120 lines in refactor"]
    },
    "surgical_changes": {
      "score": 78,
      "violations": ["Modified logging in unrelated file"],
      "scope_creep_files": ["src/logging/index.ts"],
      "out_of_spec": true
    },
    "goal_driven": {
      "score": 100,
      "violations": [],
      "evidence": ["All tests passing. RED→GREEN→REFACTOR complete."]
    },
    "overall": 89,
    "gate_decision": "PASS_WITH_APPROVAL",
    "recommendation": "Score 89: Pass scope and simplicity checks. Recommend human approval before advancing to Phase 6."
  }
}

Scoring & Gate Decisions

Overall Score Decision Action
≥90 🟢 PASS Auto-advance to next phase
70-89 🟡 PASS_WITH_APPROVAL Pause. Require human approval to advance.
<70 🔴 FAIL BLOCK. Orchestrator halts. Requires remediation + re-audit.

Scope creep detection

Compares two sources:

  1. Original spec — From docs/stories/{story}/Story.md (required scope)
  2. PR diff — Files and line changes in the current PR

Violations flagged:

  • Files modified outside the spec boundary
  • Feature branches added beyond the story requirement
  • Unrelated cleanup/refactoring

Phase 5 → Phase 6 Gate

Orchestrator behavior:

Phase 5 (IMPLEMENT) complete
  ↓
Auto-call: /karpathy-audit
  ↓
Analyze spec + PR diff + 4 principles
  ↓
Write `.claude/state/karpathy-report.json`
  ↓
Score ≥90?      → Auto-advance to Phase 6
Score 70-89?    → Pause. Display report. Require [a]pprove or [c]cancel
Score <70?      → HALT. Show violations. Require /karpathy-audit re-run after fixes

Dashboard integration

The TUI displays:

  • Phase 5 detail view — Shows karpathy-report.json if present
  • [P] overlay (Pipeline pane) — Per-principle scores + violations
  • Red blocking indicator — If score <70, blocks manual phase advance
  • Approval gate — If 70-89, shows "awaiting approval" state

Remediation workflow

If audit fails or scores <70:

1. Orchestrator shows violation details
2. Specialist agent re-assesses work against the principle
3. Fix or remove out-of-scope code
4. Manually re-run: /karpathy-audit
5. If all 4 principles ≥70 now, approve advancement

Example invocation

User: /feature "add password reset via email"
Orchestrator: [Phase 1-5 complete]
Orchestrator: Auditing against Karpathy principles...
karpathy-audit: Running audit...
[Analyze spec vs PR diff]
[Score each principle]
karpathy-audit: 📋 Report written to .claude/state/karpathy-report.json
karpathy-audit: 🟡 Score 82/100 — PASS_WITH_APPROVAL
karpathy-audit: ⚠️ Scope creep: modified src/logging (out of spec)

Dashboard: [Shows approval gate, awaiting [a] key]
User: a  [approve]
Dashboard: [Phase 6 VALIDATE begins]

Further reading

提供Kubernetes和Kustomize最佳实践,涵盖目录结构、Deployment模板、PDB、HPA及验证工作流。适用于编写或审查K8s清单文件时参考标准模式。
编写Kubernetes YAML配置 审查K8s部署清单 使用Kustomize管理环境覆盖
.claude/skills/kubernetes-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill kubernetes-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "kubernetes-patterns",
    "description": "Apply Kubernetes and Kustomize patterns for deployments, services, and overlays. Use when writing or reviewing k8s manifests."
}

SKILL: Kubernetes Patterns

Kustomize Structure

infra/k8s/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── patches/
    ├── staging/
    │   ├── kustomization.yaml
    │   └── patches/
    └── prod/
        ├── kustomization.yaml
        └── patches/

Deployment with Required Fields

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {app-name}
  labels:
    app: {app-name}
spec:
  replicas: 2
  selector:
    matchLabels:
      app: {app-name}
  template:
    metadata:
      labels:
        app: {app-name}
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
      containers:
        - name: {app-name}
          image: {image}:{tag}
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 20
          startupProbe:
            httpGet:
              path: /health
              port: 3000
            failureThreshold: 30
            periodSeconds: 10

PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {app-name}-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: {app-name}

HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {app-name}-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {app-name}
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Validation Workflow

# Always validate before applying
kubectl apply --dry-run=client -f {manifest}
kustomize build overlays/dev | kubectl apply --dry-run=client -f -
helm template {release} {chart} | kubectl apply --dry-run=client -f -
用于生成和验证Mermaid图表以记录软件架构。支持组件、序列、ER、部署及状态机图,要求语法正确且节点不超过30个,旨在规范功能文档中的架构图绘制。
需要为功能特性生成架构图 文档化系统组件关系 设计数据库实体关系 描述服务部署拓扑 梳理业务流程状态流转
.claude/skills/mermaid-diagrams/SKILL.md
npx skills add kinncj/Heimdall --skill mermaid-diagrams -g -y
SKILL.md
Frontmatter
{
    "name": "mermaid-diagrams",
    "description": "Generate and validate Mermaid diagrams (component, sequence, ER, deployment, state machine) for features. Use when documenting architecture."
}

SKILL: Mermaid Diagrams

Required Diagrams Per Feature

  1. Component diagram (graph TB)
  2. Sequence diagram (sequenceDiagram)
  3. ER diagram (erDiagram)
  4. Deployment diagram (graph TB with deployment focus)
  5. State machine (stateDiagram-v2) — where applicable

Rules

  • Maximum 30 nodes per diagram.
  • All diagrams must be syntactically valid.
  • Test diagrams in the Mermaid Live Editor before committing.
  • Use descriptive node labels.

Component Diagram

graph TB
    subgraph "Frontend"
        UI["React App"]
    end
    subgraph "Backend"
        API["REST API"]
        DB[("PostgreSQL")]
        Cache[("Redis")]
    end
    UI -->|"HTTPS"| API
    API --> DB
    API --> Cache

Sequence Diagram

sequenceDiagram
    actor U as User
    participant F as Frontend
    participant A as API
    participant D as Database

    U->>F: Action
    F->>A: POST /api/resource
    A->>D: INSERT
    D-->>A: 201 Created
    A-->>F: {id, data}
    F-->>U: Success

ER Diagram

erDiagram
    USER {
        uuid id PK
        string email UK
        timestamp created_at
    }
    ORDER {
        uuid id PK
        uuid user_id FK
        decimal total
        timestamp created_at
    }
    USER ||--o{ ORDER : "places"

State Machine

stateDiagram-v2
    [*] --> Pending
    Pending --> Processing: payment received
    Processing --> Completed: fulfilled
    Processing --> Failed: error
    Failed --> Pending: retry
    Completed --> [*]

Deployment Diagram

graph TB
    subgraph "Vercel Edge"
        FE["Next.js App"]
        EF["Edge Functions"]
    end
    subgraph "AWS"
        LB["Load Balancer"]
        APP["App Servers"]
        RDS[("RDS PostgreSQL")]
        EC["ElastiCache Redis"]
    end
    FE --> EF
    EF --> LB
    LB --> APP
    APP --> RDS
    APP --> EC
根据已审批的线框图和设计令牌,使用项目指定的UI技术栈生成高保真UI组件Mockup代码。支持React-Mantine、Tailwind/shadcn及纯HTML,需通过预检确保线框图状态和令牌文件存在。
需要实现已审批的线框图 生成高保真UI组件原型
.claude/skills/mockup/SKILL.md
npx skills add kinncj/Heimdall --skill mockup -g -y
SKILL.md
Frontmatter
{
    "name": "mockup",
    "description": "Scaffold high-fidelity UI component mockups using the project UI stack consuming approved wireframes. Use when implementing approved wireframes."
}

SKILL: mockup

Purpose

Scaffold high-fidelity UI component mockups using the project's declared UI stack (Mantine, Tailwind/shadcn, or plain HTML). Mockups consume approved wireframes and design tokens. Output is runnable code — not a design file. Human approval required before the mockup is treated as the implementation target.

Inputs

Field Source Example
story_file path to story markdown docs/stories/auth-reset-0001.md
wireframe_file path to approved wireframe docs/design/wireframes/auth-reset-0001.wireframe.md
tokens_file canonical token file docs/design/identity/tokens.json
stack project.config.yaml react-mantine | react-tailwind | html

Supported Stacks (web target)

Stack value Framework Token source
react-mantine @mantine/core mantine.theme.ts
react-tailwind Tailwind CSS + shadcn/ui tailwind.tokens.js
react-shadcn shadcn/ui + Tailwind tailwind.tokens.js
html Vanilla HTML + tokens.css tokens.css

Outputs

File Location Targets
<story-id>.mockup.md docs/design/mockups/ web + tui — metadata + approval (tui: also the terminal render + lipgloss styles)
<story-id>.mockup.tsx docs/design/mockups/ web only — React stacks
<story-id>.mockup.html docs/design/mockups/ web only — html stack

Target awareness

design.target (project.config.yaml, default web):

  • web — generate the code mockup (.tsx/.html) for the configured ui_library, plus .mockup.md.
  • tui — generate ONLY <id>.mockup.md: a fenced monospace terminal render at the target width covering default/selected/empty/error/loading states, followed by a "Styles" section mapping each region to lipgloss Foreground/Background/Border/Padding taken from docs/design/identity/terminal-theme.json. Skip the stack detector and the React templates below.

Pre-flight Checks

Before generating:

WIREFRAME="docs/design/wireframes/${STORY_ID}.wireframe.md"
TOKENS="docs/design/identity/tokens.json"

# 1. Wireframe must be approved
STATUS=$(python3 -c "
import re
m = re.search(r'^status:\s*(\w+)', open('$WIREFRAME').read(), re.MULTILINE)
print(m.group(1) if m else 'draft')
")
[ "$STATUS" = "approved" ] || { echo "BLOCKED: wireframe not approved"; exit 1; }

# 2. Tokens must exist
[ -f "$TOKENS" ] || { echo "BLOCKED: tokens.json missing — run visual-identity skill first"; exit 1; }

Mockup Template: react-mantine

// AUTO-GENERATED MOCKUP — docs/design/mockups/{story-id}.mockup.tsx
// Story: {story title}
// Wireframe: {wireframe file}
// Tokens: mantine.theme.ts
// Status: draft — awaiting approval

import { MantineProvider, Stack, TextInput, Button, Text, Alert } from '@mantine/core';
import { theme } from '../../docs/design/identity/mantine.theme';
import { IconAlertCircle } from '@tabler/icons-react';

interface Props {
  error?: string;
  onSubmit: (email: string) => void;
}

export function {ComponentName}({ error, onSubmit }: Props) {
  const [email, setEmail] = useState('');

  return (
    <MantineProvider theme={theme}>
      <Stack gap="md" maw={400} mx="auto" mt="xl">
        <Text fw={700} size="xl">{Screen Title}</Text>

        <TextInput
          label="Email"
          placeholder="you@example.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          error={error}
        />

        {error && (
          <Alert icon={<IconAlertCircle />} color="red" variant="light">
            {error}
          </Alert>
        )}

        <Button fullWidth onClick={() => onSubmit(email)}>
          Send Reset Link
        </Button>
      </Stack>
    </MantineProvider>
  );
}

Mockup Template: react-tailwind

// AUTO-GENERATED MOCKUP — docs/design/mockups/{story-id}.mockup.tsx
import { useState } from 'react';

interface Props {
  error?: string;
  onSubmit: (email: string) => void;
}

export function {ComponentName}({ error, onSubmit }: Props) {
  const [email, setEmail] = useState('');

  return (
    <div className="max-w-sm mx-auto mt-16 space-y-4">
      <h1 className="text-2xl font-bold text-foreground">{Screen Title}</h1>

      <div>
        <label className="block text-sm font-medium mb-1">Email</label>
        <input
          type="email"
          className={`w-full border rounded-md px-3 py-2 text-sm ${error ? 'border-error' : 'border-border'}`}
          placeholder="you@example.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        {error && <p className="text-error text-sm mt-1">{error}</p>}
      </div>

      <button
        className="w-full bg-brand-primary text-white rounded-md py-2 font-medium hover:opacity-90"
        onClick={() => onSubmit(email)}
      >
        Send Reset Link
      </button>
    </div>
  );
}

Mockup Metadata File

Always write a .mockup.md alongside the code file:

---
story_id: "{story_id}"
wireframe: "docs/design/wireframes/{story_id}.wireframe.md"
stack: "{stack}"
status: draft        # draft | approved | rejected
approved_by: null
approved_at: null
---

## Mockup: {story title}

### States

- Default
- Error: {describe}
- Success: {describe}
- Loading: {describe if applicable}

### Token Usage

- Primary action: `color.brand.primary`
- Error text: `color.semantic.error`
- Border: `color.surface.border`

### Approval

- [ ] Matches approved wireframe
- [ ] Design tokens applied correctly
- [ ] All interaction states present
- [ ] Approved by product owner / UX lead

Failure Modes

Condition Action
Wireframe not approved Stop. Output BLOCKED. Do not generate mockup.
tokens.json missing Stop. Run visual-identity first.
Stack unknown Default to html. Log warning.
Mockup exists and is approved Do not overwrite. Log SKIP — approved mockup locked.
Component name collision Append story ID suffix to component name.

Logging

[mockup] CREATE   docs/design/mockups/auth-reset-0001.mockup.tsx  stack=react-mantine
[mockup] CREATE   docs/design/mockups/auth-reset-0001.mockup.md   status=draft
[mockup] BLOCKED  auth-reset-0001  wireframe not approved
[mockup] SKIP     auth-reset-0001  mockup approved — locked
通用调度器,用于执行Taffy工作流、本地技能或子代理。支持从skills.sh注册表自动安装缺失技能,并在状态文件中记录进度以支持实时UI展示。
用户需要运行特定的工作流或任务 用户请求执行某个技能或代理 需要自动化编排多个步骤的任务
.claude/skills/pipeline-runner/SKILL.md
npx skills add kinncj/Heimdall --skill pipeline-runner -g -y
SKILL.md
Frontmatter
{
    "name": "pipeline-runner",
    "description": "Universal dispatcher: run a named taffy workflow (.claude\/taffy\/<name>.yaml), a skill (\/skill-name), or a sub-agent (@agent-name). Falls back to skills.sh registry when a skill is not found locally. Tracks all runs in .claude\/state\/maple.json so the maple TUI shows live progress."
}

SKILL: pipeline-runner

What It Does

Dispatches any named workflow, skill, or agent from a single entry point. Resolution order:

  1. Taffy workflow — look for .claude/taffy/<name>.yaml; if found, execute each stage in order
  2. Skill (local) — look for .claude/skills/<name>/; if found, invoke the skill
  3. Agent — look for .claude/agents/<name>.md; if found, delegate to @<name>
  4. Skill (registry) — if not found locally, try to install from skills.sh, then retry step 2

Pipeline state is written to .claude/state/maple.json at every transition.

Usage

/pipeline-runner <name>

Examples:

/pipeline-runner new-ui-feature
/pipeline-runner api-endpoint
/pipeline-runner implement-stories
/pipeline-runner tdd-workflow
/pipeline-runner orchestrator

List available taffy workflows:

ls .claude/taffy/*.yaml | grep -v schema

List available skills:

ls .claude/skills/

Dispatch Protocol

Step 1: Resolve the target

# Check taffy first
[ -f ".claude/taffy/<name>.yaml" ] && dispatch=taffy
# Then local skill
[ -d ".claude/skills/<name>" ] && dispatch=skill
# Then agent
[ -f ".claude/agents/<name>.md" ] && dispatch=agent
# Fallback: fetch from skills.sh registry, then retry
if [ -z "$dispatch" ] && command -v npx &>/dev/null; then
  echo "pipeline-runner: '<name>' not found locally — checking skills.sh…"
  npx --yes skills add kinncj/maple@<name> -a claude-code -y 2>/dev/null \
    || npx --yes skills add <name> -a claude-code -y 2>/dev/null \
    || true
  [ -d ".claude/skills/<name>" ] && dispatch=skill
fi

If nothing matches after the registry fallback, report: pipeline-runner: no taffy workflow, skill, or agent named '<name>' (also checked skills.sh registry)

Step 2: Initialise state

Write to .claude/state/maple.json (merge — do not overwrite unowned fields):

{
  "taffy": "<name>",
  "stage": "<first-stage or skill-name>",
  "status": "RUNNING",
  "awaiting_approval": null,
  "started_at": "<iso8601>",
  "updated_at": "<iso8601>"
}

Step 2b: Runtime policy enforcement (mandatory)

Before any stage execution, read and enforce:

  • Harness-specific root markdown:
    • Claude harness → CLAUDE.md
    • OpenCode harness → OPENCODE.md
    • Cursor harness → CURSOR.md
    • Copilot harness → COPILOT.md
  • AGENTS.md
  • .github/copilot-instructions.md
  • .github/instructions/stories.instructions.md (when touching story files)

When the launch prompt contains a <maple-gherkin-handoff> block:

  1. Treat it as hard scope: implement only listed story paths / IDs.
  2. Do not run Spec-Kit or regenerate stories.
  3. Preserve the repository's current Cucumber stack:
    • If generated stories include cucumber/*_steps.py, use Python behave-style steps.
    • Do not introduce TypeScript @cucumber/cucumber unless the repository already uses it as the active standard.
  4. Keep BusinessRepo structure and phase gates exactly as defined by instruction files.
  5. Treat test layout as mandatory:
    • Gherkin feature files must live under /tests/features/.
    • Step definitions must use the repository's active Cucumber stack and live under /tests.
    • Do not place acceptance tests under /app or story directories.
  6. Enforce module boundaries independent of language:
    • Runtime/source files must not import from docs/, .github/, or .claude/.
    • Copying/adapting approved artifact content into app/test source is allowed.
    • Design/spec artifacts are references, not runtime code dependencies.

Step 3a: Taffy workflow execution

Load .claude/taffy/<name>.yaml, parse stages:, resolve depends_on order.

If the workflow defines an orchestrator-kickoff stage, execute it first before all other stages. This stage must publish the initial plan and heartbeat cadence.

For each stage:

when: guard:

  • when: ui:true — skip if story has ui: false, unless .claude/state/maple.json has force_ui: true with launch_source: maple-x (MAPLE [x] quick-launch override).
  • when: ui:false — skip if story has ui: true
  • when: always — always run
  • UI-related scope includes web, mobile, desktop, and TUI stories. Any such run must include design-review human-approval stages before implementation phases.

depends_on: all listed stages must be DONE before this one starts.

Dispatch:

  • agent: <name> → delegate to @<name> with current story context
  • skill: <name> → invoke the skill
  • pipeline: standard → run the full 8-phase orchestrator pipeline

After each stage: update maple.json with current stage + RUNNING.

Progress heartbeats (mandatory):

  • Send an immediate kickoff status before the first long-running tool/agent call.
  • While a taffy run is active, send a concise progress update at least every 60-120 seconds.
  • On each heartbeat, refresh maple.json updated_at and current stage.
  • Every heartbeat must include concrete progress evidence:
    • changed files/artifacts since last update (explicit paths), or
    • a specific blocker that prevented changes.
  • Use this status format:
    • Progress: <stage / phase>
    • Done since last update: <brief>
    • Current action: <brief>
    • Blockers: <none or blocker>
    • Next update: <ETA>
  • Do not send heartbeat-only timestamp churn with no artifact/blocker details.
  • If a stage requires writing artifacts and write access/tools are unavailable, set maple.json to FAILED with an explicit error and stop.
  • If blocked/waiting, report what is pending and continue heartbeats until unblocked.

Completion artifact gate (mandatory):

  • Before marking DONE, verify the run produced concrete story-linked artifacts under the BusinessRepo layout.
  • Required for implementation runs:
    • application changes in /app (or existing domain folders),
    • tests in /tests (unit/integration/e2e as applicable),
    • Gherkin assets in /tests/features plus matching step implementations.
  • Boundary check:
    • fail the run if generated runtime code imports paths under docs/, .github/, or .claude/.
  • If required test/gherkin artifacts are missing, set maple.json to FAILED and report missing paths explicitly.

Step 3b: Skill invocation

Invoke the skill directly. Update maple.json on start and completion.

Step 3c: Agent delegation

Delegate to @<name>. Update maple.json on start and completion.

Step 4: Human-approval gates (taffy only)

When a stage has gate: human-approval:

  1. Complete stage work (produce artifact).
    • For design review stages (wireframe, visual-identity, design-tokens, ui-mockup-builder, design-refresh), artifact production is mandatory:
      • create at least one previewable artifact (.excalidraw, .html, .svg, .png, .jpg, .jpeg, .webp, or .md) under docs/design (or approved artifact dirs), and
      • the required formats depend on design.target (project.config.yaml, default web): web = .md + .html + .excalidraw; tui = .md + .excalidraw (no .html). A run that produces fewer than the required formats is incomplete, and
      • update .claude/state/design-artifacts.json with current stage artifact paths so the review portal can update live.
    • Path + completeness gate for wireframe stage (mandatory before PAUSING):
      # Verify wireframes are in the canonical location and the required formats exist.
      # Required formats depend on design.target (web: md+html+excalidraw; tui: md+excalidraw).
      TARGET=$(sed -n 's/^[[:space:]]*target:[[:space:]]*\([a-z]*\).*/\1/p' project.config.yaml 2>/dev/null | head -1)
      [ -z "$TARGET" ] && TARGET=web
      CANONICAL=$(find docs/design/wireframes -name "*.wireframe.*" 2>/dev/null | wc -l | tr -d ' ')
      MISPLACED=$(find docs -name "*.wireframe.*" -not -path "*/docs/design/wireframes/*" 2>/dev/null)
      MISSING_HTML=""
      if [ "$TARGET" != "tui" ]; then
        MISSING_HTML=$(find docs/design/wireframes -name "*.wireframe.md" 2>/dev/null | while read md; do
          base="${md%.wireframe.md}"
          [ ! -f "${base}.wireframe.html" ] && echo "${base}.wireframe.html"
        done)
      fi
      MISSING_EXCALIDRAW=$(find docs/design/wireframes -name "*.wireframe.md" 2>/dev/null | while read md; do
        base="${md%.wireframe.md}"
        [ ! -f "${base}.wireframe.excalidraw" ] && echo "${base}.wireframe.excalidraw"
      done)
      if [ "$CANONICAL" -eq 0 ]; then
        if [ -n "$MISPLACED" ]; then
          echo "PIPELINE GATE FAILED: wireframes found at wrong path(s): $MISPLACED"
          echo "Canonical path is docs/design/wireframes/ — move files there before this stage can complete."
        else
          echo "PIPELINE GATE FAILED: no wireframe artifacts in docs/design/wireframes/"
        fi
        # set maple.json FAILED and stop
      fi
      if [ -n "$MISSING_HTML" ]; then
        echo "PIPELINE GATE FAILED: missing .wireframe.html for: $MISSING_HTML — generate it now."
        # set maple.json FAILED and stop
      fi
      if [ -n "$MISSING_EXCALIDRAW" ]; then
        echo "PIPELINE GATE FAILED: missing .wireframe.excalidraw for: $MISSING_EXCALIDRAW — generate it now."
        # set maple.json FAILED and stop
      fi
      
      If the gate fails, produce the missing files before re-running the gate check.
    • If no reviewable artifact exists for a design gate, set maple.json to FAILED and stop.
  2. Write PAUSED state:
{ "stage": "<name>", "status": "PAUSED", "awaiting_approval": "<name>", "updated_at": "<iso8601>" }
  1. Write stage name to .claude/state/approval-pending.txt.
  2. Output:
TAFFY PAUSED — awaiting human approval
Stage:    <stage-name>
Artifact: <artifact path or description>

Approve via the maple TUI ([P] pipeline → [a] approve) or reply "approved" / "continue".
I will not advance to the next stage until approval is confirmed.
  1. Poll: timeout 540 bash -c 'until [ ! -f .claude/state/approval-pending.txt ]; do sleep 2; done'
    • On timeout (exit 124), re-run the same poll. The Bash tool caps at 10 min per call; re-polling across calls lets approval delays exceed that bound.
    • Also accept an explicit "approved" / "continue" reply in chat.
    • When the user approves via the maple TUI ([P] → [a]), the TUI deletes the pending file and sends a "continue" keystroke to the agent's pane via the active multiplexer (outer tmux/zellij, or a detached tmux new-session wrapper). Either signal is sufficient to resume.
  2. On resume: update to RUNNING, advance to next stage.
  3. While paused, monitor .claude/state/design-feedback.json:
    • status: requested_changes or status: rejected means apply the requested updates before advancing.
    • Treat attachments as required review inputs (uploaded files such as .excalidraw, images, HTML, text), typically under docs/design/review-input/.
    • Summarize how each feedback item and attachment was addressed before continuing.

Step 5: Completion

{ "taffy": "<name>", "stage": "DONE", "status": "DONE", "awaiting_approval": null, "updated_at": "<iso8601>" }

Output:

TAFFY COMPLETE — <name>
Stages run: N
Duration:   <elapsed>

Failure Handling

After 3 consecutive failures on any stage:

{ "stage": "<name>", "status": "FAILED", "error": "<summary>", "updated_at": "<iso8601>" }

Stop. Report failed stage and error to human. Do not proceed.

Rate-limit Handling

When a usage limit, rate limit, HTTP 429, or "resets at

  1. Merge-write .claude/state/maple.json (keep taffy/stage):
{ "status": "RATE_LIMITED", "resume_at": "<iso8601 reset time, else now+1h>", "rate_limit_reason": "<one line>", "updated_at": "<iso8601>" }
  1. Write a one-line breadcrumb to .claude/state/rate-limit.txt: <resume_at>|<reason>. One cheap write — do it even if nothing else is possible.
  2. Stop. Do not hammer the API. MAPLE resumes the pipeline when the window clears (manual [r], or auto when armed).

Session Context

On startup, read .claude/state/sessions.json if it exists:

{ "claude": "<uuid>", "opencode": "<id>", "copilot": "<id>" }

Use the matching session ID when resuming work within an existing agent session.

State File Reference

All state in .claude/state/. TUI and skill share these files.

.claude/state/maple.json

Field Owner Values
taffy skill workflow/skill/agent name
stage skill current stage name
status skill RUNNING, PAUSED, DONE, FAILED
awaiting_approval skill local MAPLE stage name (e.g., spec-kit = Specification Knowledge & Integration Toolkit) or null — not an external package reference
pipeline skill standard if running 8-phase
started_at skill ISO 8601
updated_at skill ISO 8601
resume_at skill/TUI ISO 8601 — when the rate-limit window clears
rate_limit_reason skill/TUI one-line cause
harness TUI which harness ran this pipeline (for resume)
auto_resume TUI true when auto-resume is armed
state TUI running or exited
ts TUI ISO 8601

Merge-not-overwrite: read existing file, update only owned fields, re-write.

.claude/state/approval-pending.txt

Skill writes stage name. TUI deletes when user presses [a].

.claude/state/rate-limit.txt

Skill/agent writes <resume_at>|<reason>. TUI reads, promotes maple.json to RATE_LIMITED, and deletes the file.

.claude/state/sessions.json

TUI writes harness→session-ID map. Skill reads for session resume.

Skip Conditions

  • spike/* and chore/* branches: skip Spec-Kit stages, run implementation stages.
  • Stage when: ui:true on a ui: false story: skip silently, log [pipeline-runner] SKIP stage=<name> reason=ui:false unless quick-launch override is active (force_ui=true, launch_source=maple-x).
提供Playwright CLI交互探索、基于快照的页面操作及会话管理功能,支持将交互过程转换为E2E测试代码,并涵盖测试执行、可视化调试及API测试能力。
需要进行浏览器自动化测试 需要交互式探索网页元素 需要将手动操作转换为测试脚本 需要运行或调试E2E测试套件
.claude/skills/playwright-cli/SKILL.md
npx skills add kinncj/Heimdall --skill playwright-cli -g -y
SKILL.md
Frontmatter
{
    "name": "playwright-cli",
    "description": "Use Playwright CLI for interactive browser exploration and snapshot-based interaction. Use when writing or running browser automation tests."
}

SKILL: Playwright CLI

When to Use

Use Playwright CLI (playwright-cli) for interactive browser exploration and snapshot-based interaction. Use npx playwright test for running actual test suites.

Installation

npm install -g @playwright/cli
npx playwright install  # installs browsers

Core CLI Commands

Browser Exploration

# Open browser to URL
playwright-cli open http://localhost:3000

# Take accessibility snapshot (YAML with element references)
playwright-cli snapshot
# Output: saves to disk as snapshot.yml — review before acting

# Interact by element reference (e.g., e21 from snapshot)
playwright-cli click e21
playwright-cli fill e35 "test@example.com"
playwright-cli press e35 Enter
playwright-cli screenshot  # saves to disk

Session Management

# Named sessions for auth state
playwright-cli --session=auth open http://localhost:3000/login
playwright-cli --session=auth fill e10 "admin@example.com"
playwright-cli --session=auth fill e11 "password"
playwright-cli --session=auth click e20  # submit button
playwright-cli session-list

Snapshot-to-Test Workflow

  1. playwright-cli open http://localhost:3000
  2. playwright-cli snapshot → review element refs in snapshot.yml
  3. Interact: playwright-cli click e21, playwright-cli fill e35 "value"
  4. Observe results
  5. Convert to test file:
// tests/e2e/feature.spec.ts
import { test, expect } from '@playwright/test';

test('should {outcome} when {action}', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Test Execution

# Run all E2E tests
npx playwright test tests/e2e/

# Run specific test file
npx playwright test tests/e2e/auth.spec.ts

# Visual debugger
npx playwright test --ui

# Headed mode (visible browser)
npx playwright test --headed

# Generate tests by recording
npx playwright codegen http://localhost:3000

# View HTML report
npx playwright show-report

API Testing (Playwright request fixture)

test('POST /api/users returns 201', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: {
      email: 'test@example.com',
      name: 'Test User'
    }
  });
  expect(response.status()).toBe(201);
  const body = await response.json();
  expect(body).toHaveProperty('id');
});

Token Efficiency Note

Use playwright-cli snapshot (saves to disk) instead of screenshots in context. The compact YAML element refs (~1,350 tokens) vs accessibility tree injection (~5,700 tokens).

提供PostgreSQL模式、迁移和查询的最佳实践,包括表设计、RLS安全、索引优化及PgBouncer配置。指导编写幂等SQL,遵循版本控制规则,确保高性能与数据安全。
编写数据库表结构或列定义 生成或执行数据库迁移脚本 优化复杂SQL查询性能 配置连接池参数
.claude/skills/postgresql-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill postgresql-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "postgresql-patterns",
    "description": "Apply PostgreSQL schema, migration, and query patterns with idempotent SQL. Use when writing database schemas or migrations."
}

SKILL: PostgreSQL Patterns

Migration Naming

migrations/
  V001__create_users.sql          # Flyway
  0001_create_users.sql           # Generic
  20240115_create_users.sql       # Timestamp-based

Table Pattern with RLS

-- Create table
CREATE TABLE IF NOT EXISTS users (
  id          uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  email       text UNIQUE NOT NULL,
  name        text NOT NULL,
  role        text NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
  created_at  timestamptz DEFAULT now() NOT NULL,
  updated_at  timestamptz DEFAULT now() NOT NULL
);

-- Indexes
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_users_role ON users(role) WHERE role != 'user';

-- Updated_at trigger
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_updated_at
  BEFORE UPDATE ON users
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY users_select_own ON users
  FOR SELECT USING (id = current_user_id());

CREATE POLICY users_admin_all ON users
  FOR ALL USING (current_user_role() = 'admin');

Query Optimization

-- Always EXPLAIN ANALYZE in development
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > now() - interval '30 days'
GROUP BY u.id;

-- Partial index for common filter
CREATE INDEX idx_orders_pending
  ON orders(created_at)
  WHERE status = 'pending';

-- GIN index for full-text search
CREATE INDEX idx_products_search
  ON products USING gin(to_tsvector('english', name || ' ' || description));

Connection Pooling (PgBouncer)

# pgbouncer.ini
[databases]
app = host=127.0.0.1 port=5432 dbname=app

[pgbouncer]
pool_mode = transaction  # transaction pooling for most apps
max_client_conn = 100
default_pool_size = 20

Rules

  • NEVER modify existing migration files.
  • Always create new migration for schema changes.
  • Index all foreign keys.
  • Use CONCURRENTLY for index creation on large tables.
  • EXPLAIN ANALYZE all queries > 100ms.
  • Enable RLS on tables with user data.
提供Redis集成最佳实践,涵盖键命名规范、Cache-Aside缓存模式、基于Sorted Set的限流及Session存储方案。强调设置TTL、使用SCAN和管道操作等规则,确保高性能与稳定性。
需要实现数据缓存逻辑 设计API限流功能 配置会话状态存储 优化Redis键名管理
.claude/skills/redis-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill redis-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "redis-patterns",
    "description": "Apply Redis data structure, caching, and pub\/sub patterns. Use when integrating Redis into a service."
}

SKILL: Redis Patterns

Key Design Convention

{app}:{domain}:{entity}:{id}
# Examples:
myapp:user:session:usr_123
myapp:product:cache:prod_456
myapp:rate:api:ip_1.2.3.4
myapp:queue:emails:pending

Cache-Aside Pattern

async function getUser(id: string): Promise<User> {
  const key = `myapp:user:data:${id}`;
  const ttl = 3600; // 1 hour

  // Try cache first
  const cached = await redis.get(key);
  if (cached) {
    return JSON.parse(cached) as User;
  }

  // Cache miss — fetch from DB
  const user = await db.users.findById(id);
  if (!user) throw new Error('User not found');

  // Store in cache with TTL
  await redis.setex(key, ttl, JSON.stringify(user));
  return user;
}

// Invalidate on update
async function updateUser(id: string, data: Partial<User>): Promise<User> {
  const user = await db.users.update(id, data);
  await redis.del(`myapp:user:data:${id}`);
  return user;
}

Rate Limiting with Sorted Set

async function rateLimit(ip: string, limit: number, windowSeconds: number): Promise<boolean> {
  const key = `myapp:rate:api:${ip}`;
  const now = Date.now();
  const windowStart = now - (windowSeconds * 1000);

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart);
  pipeline.zadd(key, now, `${now}`);
  pipeline.zcard(key);
  pipeline.expire(key, windowSeconds);

  const results = await pipeline.exec();
  const count = results?.[2]?.[1] as number;
  return count <= limit;
}

Session Store

// Using ioredis with connect-redis
import session from 'express-session';
import RedisStore from 'connect-redis';

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24, // 24 hours
  }
}));

Rules

  • ALWAYS set TTLs on cache keys — never store indefinitely.
  • Use SCAN not KEYS in production (non-blocking).
  • Use pipelining for multiple operations.
  • Key convention: {app}:{domain}:{entity}:{id}.
  • Monitor with: redis-cli info memory and redis-cli monitor (dev only).
  • Set maxmemory-policy allkeys-lru for cache-only Redis instances.
用于创建和管理架构决策记录(ADR),规范存储于docs/specs/目录。涵盖格式模板及适用场景,旨在记录重大技术选型、模式变更及其权衡利弊,确保决策可追溯并评估对成本、运维、安全的影响。
需要记录重大架构决策 在多个非平凡技术方案间进行选择 引入新技术或框架 改变现有显著模式
.claude/skills/rfc-adr/SKILL.md
npx skills add kinncj/Heimdall --skill rfc-adr -g -y
SKILL.md
Frontmatter
{
    "name": "rfc-adr",
    "description": "Author and manage Architecture Decision Records (ADRs) in docs\/specs\/. Use when a significant architectural decision needs to be documented."
}

SKILL: RFC / Architecture Decision Records

ADR Format

Every significant architectural decision gets an ADR. Store in: docs/specs/{feature}/adr.md

# ADR-{N}: {Short Title}

## Status
Proposed | Accepted | Deprecated | Superseded by ADR-{N}

## Context
What is the problem or situation that requires a decision?
What constraints exist?

## Goals
- {Specific, measurable goal}

## Non-goals
- {What this ADR does NOT address}

## Proposal
{Detailed technical proposal. Include code snippets if helpful.}

## Alternatives Considered

### Option A: {Name}
**Description:** {What it is}
**Pros:** {list}
**Cons:** {list}

### Option B: {Name}
**Description:** {What it is}
**Pros:** {list}
**Cons:** {list}

## Trade-offs and Risks
{Analysis of trade-offs. What could go wrong?}

## Impact

### Cost (FinOps)
{Estimated cloud cost impact. Monthly estimate if possible.}

### Operations (SRE)
{New runbook requirements. Alert thresholds. On-call implications.}

### Security
{Changes to attack surface. New threat vectors. Compliance implications.}

### Team
{Skill requirements. Training needed. Hiring implications.}

## Decision
{Final decision and rationale. Who made it and when.}

## Next Steps
- [ ] {Action item with owner}
- [ ] {Action item with owner}

When to Write an ADR

  • Choosing between two or more non-trivial technical approaches.
  • Adopting a new technology or framework.
  • Changing a significant existing pattern.
  • Making a trade-off with known long-term implications.

When NOT to Write an ADR

  • Obvious choices with no real alternatives.
  • Implementation details within an agreed-upon approach.
  • Temporary decisions expected to change soon.
提供独立的代码、计划或测试审查,用于在关键阶段获取第二意见。支持计划、代码和测试三种模式,输出CRITICAL/WARN/INFO列表及批准或请求更改的裁决,帮助发现遗漏问题。
完成规划后 复杂多文件实现后 编写测试后运行前 感到不确定时
.claude/skills/rubber-duck/SKILL.md
npx skills add kinncj/Heimdall --skill rubber-duck -g -y
SKILL.md
Frontmatter
{
    "name": "rubber-duck",
    "description": "Invoke the rubber duck reviewer for a second opinion. Use after planning, after complex multi-file implementations, or after writing tests. Produces a focused CRITICAL\/WARN\/INFO list and a APPROVE\/REQUEST_CHANGES verdict."
}

rubber-duck skill

Invoke @rubber-duck to get an independent second opinion on a plan, implementation, or test suite. This is the same pattern as GitHub Copilot CLI's built-in Rubber Duck feature — a different perspective to catch what the primary agent missed.

When to invoke

Checkpoint What to pass What you get back
After Phase 3 (plan complete) plan.md + test-plan.md PLAN REVIEW
After complex multi-file implementation Changed files / diff CODE REVIEW
After tests written, before running Test files TEST REVIEW
Any time you feel uncertain Anything relevant Focused critique

How to call it

@rubber-duck Mode: PLAN REVIEW

Plan: <paste plan.md content or path>
Story: <paste story frontmatter + scenarios>
@rubber-duck Mode: CODE REVIEW

Files changed:
- src/scheduler.ts
- src/jobs/notify.ts
- src/queue/index.ts

<paste diff or file content>
@rubber-duck Mode: TEST REVIEW

Test files:
- tests/unit/scheduler.test.ts
- tests/e2e/notify.spec.ts
- features/scheduler.feature

<paste test content>

Verdict handling

Verdict Orchestrator action
APPROVE Proceed to next phase
REQUEST_CHANGES Send findings to the responsible agent for revision; re-review once

CRITICAL findings always block. WARN findings are surfaced to the human for a decision. INFO findings are logged but do not block.

Tips

  • You do not need to prepare anything special — just pass the relevant content.
  • The rubber duck will not rewrite your code. It only surfaces findings. Acting on them is your job.
  • For Copilot CLI users: the built-in /experimental Rubber Duck activates automatically at the same checkpoints. This skill adds equivalent coverage when using Claude Code or OpenCode.
在项目提交前运行安全与质量审计,扫描密钥泄露、漏洞及风险模式。需启用环境变量后使用,根据严重程度(高/中/低)分类报告结果,阻断高危问题后再合并代码。
打开 Pull Request 前 添加新依赖后 功能分支合并到主分支前 修改认证、密钥处理或基础设施配置后
.claude/skills/ship-safe/SKILL.md
npx skills add kinncj/Heimdall --skill ship-safe -g -y
SKILL.md
Frontmatter
{
    "name": "ship-safe",
    "description": "Run ship-safe security and quality audit on the current project. Executes npx ship-safe audit . and reports findings by severity. Use before shipping any feature or PR."
}

SKILL: Ship-Safe Audit

What It Does

Runs ship-safe — a pre-ship security and quality scanner that checks for secrets, vulnerabilities, and risky patterns before code reaches production.

Usage

npx ship-safe audit .

Run from the project root. No install required (npx fetches it on demand).

Opt-In

Ship-safe is disabled by default. To enable it:

  • CI/CD: set the repository variable ENABLE_SHIP_SAFE=true in GitHub → Settings → Variables
  • Agents / local: set env var ENABLE_SHIP_SAFE=true before invoking /ship-safe

When to Use

Only run if ENABLE_SHIP_SAFE=true is set. When enabled, appropriate moments are:

  • Before opening a PR
  • After adding new dependencies
  • Before merging any feature branch to main
  • After touching auth, secrets handling, or infra config

Output Interpretation

Symbol / keyword Severity Action
✓ PASS / ok / no issues Clean Safe to ship
⚠ WARN / MEDIUM / LOW Advisory Review before shipping
✗ FAIL / ERROR / CRITICAL / HIGH Blocker Must fix before shipping

Agent Instructions

  1. Run npx ship-safe audit . from the repo root.
  2. Parse stdout for CRITICAL/HIGH findings — these are blockers.
  3. For each blocker: report the file, line, and finding description.
  4. For MEDIUM/LOW: report as advisory, do not block.
  5. If all checks pass: confirm "ship-safe: clean" and proceed.
  6. If blockers found: halt the task, report findings to Orchestrator.

Example Integration (architect / pre-ship checklist)

/ship-safe

The skill runs the audit, colors findings by severity, and surfaces blockers before any merge action.

用于生成符合规范的 Gherkin 故事文件,作为功能的最小规格说明。支持状态机流程、模板填充、场景数量指导及自动化验证,确保开发前完成需求定义与审批。
需要为新功能编写规格说明 验证故事文件是否已获批且包含有效 Gherkin 场景
.claude/skills/spec-kit/SKILL.md
npx skills add kinncj/Heimdall --skill spec-kit -g -y
SKILL.md
Frontmatter
{
    "name": "spec-kit",
    "description": "Write a Gherkin story file to docs\/stories\/ for a feature. The story IS the spec. No intermediate PROBLEM\/SPEC\/PLAN\/TASKS artifacts."
}

SKILL: spec-kit

Purpose

Produce a complete Gherkin story file in docs/stories/ before any implementation begins. One invocation = one story file. The story file is the minimum spec — no additional artifacts required.

State Machine

feature description → story draft → human approval → DISCOVER

No step may begin until the previous one completes. Approval = human sets status: approved in the story frontmatter, or confirms verbally.

Story File Location

docs/stories/{epic-slug}-{feature-slug}.md   # with epic context
docs/stories/{feature-slug}.md               # standalone feature

Story File Template

---
id: "{feature-slug}"
title: "{Feature Title}"
epic: "{epic-slug or null}"
priority: "high|medium|low"
ui: false
adr_required: false
phase: discover
labels:
  - "type:feature"
  - "priority:{priority}"
status: draft
---

## Story

**As a** {user role},
**I want** {what they want},
**so that** {business outcome}.

## Acceptance Criteria

```gherkin
@story:{feature-slug} @priority:{priority}
Feature: {Feature Title}

  Scenario: {happy path title}
    Given {precondition}
    When {action}
    Then {outcome}

  Scenario: {failure or edge case title}
    Given {precondition}
    When {action}
    Then {outcome}

Definition of Done

  • All Gherkin scenarios have passing step implementations
  • Unit tests written and passing
  • Code reviewed and approved
  • Documentation updated if user-facing

## Validate a Story File

```bash
validate_story() {
  local file="$1"

  if [ ! -f "$file" ]; then
    echo "[spec-kit] MISSING  $file"
    return 1
  fi

  if ! grep -qE "^\s*(Scenario|Scenario Outline):" "$file"; then
    echo "[spec-kit] FAIL  $file — no Gherkin scenarios found"
    return 1
  fi

  STATUS=$(grep -m1 "^status:" "$file" | awk '{print $2}' | tr -d '"')
  if [ "$STATUS" != "approved" ]; then
    echo "[spec-kit] BLOCKED  $file  status=$STATUS  required=approved"
    return 1
  fi

  echo "[spec-kit] OK  $file  status=approved"
}

# Example
validate_story "docs/stories/user-auth-reset-password.md" || exit 1

Check All Stories Are Approved

FAIL=0
STORIES=$(find docs/stories -name "*.md" ! -name "_template.md" 2>/dev/null || true)

if [ -z "$STORIES" ]; then
  echo "[spec-kit] SKIP  no story files found"
  exit 0
fi

for story in $STORIES; do
  validate_story "$story" || FAIL=1
done

exit $FAIL

Scenario Count Guidelines

Feature complexity Minimum scenarios
Simple CRUD 2 (happy path + not-found/validation failure)
Auth / permissions 3 (success + unauthorized + invalid input)
Multi-step workflow 1 per step + 1 end-to-end
UI component 2 (renders correctly + interaction)

Do not invent scenarios not implied by the feature description. Mark unknown cases as TODO and flag for human review.

Skip Conditions

BRANCH=$(git branch --show-current)
if echo "$BRANCH" | grep -qE '^(spike|chore)/'; then
  echo "[spec-kit] SKIP  branch=$BRANCH — spike/chore exempt"
  exit 0
fi

Failure Modes

Condition Action
No feature description given Ask the human before writing anything
Story has zero Scenario: blocks Refuse to emit. Require at least one scenario.
Story status: rejected Surface rejection reason. Do not advance.
Duplicate story file exists Confirm with human before overwriting.

Logging

[spec-kit] WRITE   docs/stories/user-auth-reset-password.md
[spec-kit] HALT    awaiting human approval
[spec-kit] OK      docs/stories/user-auth-reset-password.md  status=approved
[spec-kit] SKIP    spike/* branch — spec-kit not required
用于评估新功能上线前的运营就绪状态。涵盖故障模式分析、可观测性指标与日志规范、告警策略及回滚计划,确保生产环境稳定性。
新功能发布前评审 检查服务上线准备度
.claude/skills/sre-review/SKILL.md
npx skills add kinncj/Heimdall --skill sre-review -g -y
SKILL.md
Frontmatter
{
    "name": "sre-review",
    "description": "Evaluate operational readiness: SLOs, alerting, runbooks, and rollback plan before launch. Use when reviewing a feature for production readiness."
}

SKILL: SRE Review

Purpose

Evaluate operational readiness of new features before launch.

Failure Mode Analysis

For each new component, document:

  1. What can fail? (service down, latency spike, data corruption, cascade)
  2. Blast radius? (who is affected? how many users?)
  3. Detection time? (how quickly will alerts fire?)
  4. Recovery time? (how long to restore service?)
  5. Can it be rolled back? (feature flag? migration rollback?)

Observability Requirements

Metrics (Prometheus/CloudWatch/Datadog)

{feature}_requests_total{status="success|error"} counter
{feature}_request_duration_seconds histogram
{feature}_active_connections gauge
{feature}_errors_total{type="validation|timeout|upstream"} counter

Logs (Structured JSON)

{
  "timestamp": "ISO8601",
  "level": "info|warn|error",
  "service": "{service-name}",
  "trace_id": "{distributed-trace-id}",
  "user_id": "{anonymized}",
  "action": "{what happened}",
  "duration_ms": 42,
  "result": "success|error",
  "error": "{message if error}"
}

Traces

  • Instrument all cross-service calls with OpenTelemetry.
  • Trace IDs propagated via W3C Trace Context headers.

Alerts

Alert Condition Severity Action
High error rate error_rate > 1% for 5m P1 Page on-call
Slow responses p99 > 2s for 10m P2 Notify team
Saturation CPU > 80% for 15m P2 Scale out

Runbook Template

docs/runbooks/{feature}-runbook.md

# Runbook: {Feature Name}

## Symptoms
- {Alert name}: {what the user sees}

## Diagnosis
1. Check logs: `kubectl logs -l app={service} --tail=100`
2. Check metrics: {dashboard URL}
3. Check dependencies: {health check commands}

## Mitigation
### Option A: Restart service
```bash
kubectl rollout restart deployment/{service}
kubectl rollout status deployment/{service}

Option B: Feature flag off

# Disable via environment variable
kubectl set env deployment/{service} FEATURE_{NAME}_ENABLED=false

Escalation

  • On-call: {PagerDuty rotation}
  • Escalation: {eng manager}
  • War room: {Slack channel}
维护故事Markdown文件与GitHub Issue的双向一致性。根据所有权模型同步标题、正文、标签、状态及DoD检查清单,支持创建新Issue并回填编号,或在文件更新时同步Issue内容。
需要创建新的Story并关联GitHub Issue时 Story文件的Gherkin或验收标准发生更改需同步至Issue时 需要保持本地文件与Issue的标签、状态和元数据一致时
.claude/skills/story-issue-sync/SKILL.md
npx skills add kinncj/Heimdall --skill story-issue-sync -g -y
SKILL.md
Frontmatter
{
    "name": "story-issue-sync",
    "description": "Maintain bidirectional consistency between story markdown files and GitHub Issues. Use when syncing story status with GitHub."
}

SKILL: story-issue-sync

Purpose

Maintain bidirectional consistency between story files (docs/stories/*.md) and GitHub Issues. The file is authoritative for narrative and Gherkin. The issue is authoritative for labels, status, project board position, and DoD checklist state.

Ownership Model

Attribute Authoritative source Sync direction
Title Story file (frontmatter title) File → Issue
Body / Gherkin Story file File → Issue (on update)
issue_number Issue (assigned at create) Issue → File
issue_url Issue Issue → File
Labels Issue Issue → File (labels frontmatter)
Status / Phase Issue labels Issue → File (labels frontmatter)
DoD checklist Issue body (task list) Issue → File (on review)
Project board position Project v2 Not persisted to file

Inputs / Outputs

Field Source Notes
story_file docs/stories/*.md Path to the story markdown file
issue_number Issue (post-create) Written back to story frontmatter
issue_node_id gh issue create --json id Passed to gh-projects skill for board add
issue_url Issue (post-create) Written back to story frontmatter

Create: File → Issue

When a new story file exists with issue_number: null:

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

# Extract frontmatter fields
TITLE=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.search(r'^title:\s*[\"\'](.*?)[\"\']', text, re.MULTILINE)
print(m.group(1) if m else '')
")

LABELS=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.findall(r'^\s+- [\"\'](.*?)[\"\']\s*$', text, re.MULTILINE)
print(','.join(m))
")

MILESTONE=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.search(r'^milestone:\s*[\"\'](.*?)[\"\']', text, re.MULTILINE)
print(m.group(1) if m else '')
")

# Create the issue
RESULT=$(gh issue create \
  --title "$TITLE" \
  --body-file "$STORY_FILE" \
  --label "$LABELS" \
  --milestone "$MILESTONE" \
  --json number,url,id)

ISSUE_NUMBER=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['number'])")
ISSUE_URL=$(echo "$RESULT"    | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")

# Write back to story frontmatter
python3 - <<EOF
import re
text = open('$STORY_FILE').read()
text = re.sub(r'^issue_number: null', f'issue_number: $ISSUE_NUMBER', text, flags=re.MULTILINE)
text = re.sub(r'^issue_url: null', f'issue_url: "$ISSUE_URL"', text, flags=re.MULTILINE)
open('$STORY_FILE', 'w').write(text)
EOF

echo "[story-sync] CREATED  #$ISSUE_NUMBER  ← $STORY_FILE"

Update: File → Issue (body changed)

When story Gherkin or acceptance criteria are edited:

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

gh issue edit "$ISSUE_NUMBER" \
  --body-file "$STORY_FILE"

echo "[story-sync] BODY_UPDATE  #$ISSUE_NUMBER  ← $STORY_FILE"

Sync: Issue → File (labels changed)

When labels on the issue change (e.g., after PO updates priority or phase advances):

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

# Fetch current labels from issue
CURRENT_LABELS=$(gh issue view "$ISSUE_NUMBER" \
  --json labels \
  --jq '[.labels[].name] | join(",")')

# Rewrite labels block in frontmatter
python3 - <<EOF
import re
text = open('$STORY_FILE').read()
label_lines = '\n'.join(f'  - "{l}"' for l in "$CURRENT_LABELS".split(',') if l)
text = re.sub(
    r'^labels:\n(  - .*\n)+',
    f'labels:\n{label_lines}\n',
    text,
    flags=re.MULTILINE
)
open('$STORY_FILE', 'w').write(text)
EOF

echo "[story-sync] LABELS_SYNC  #$ISSUE_NUMBER  → $STORY_FILE"

Detect Drift (file vs issue)

Run before any update to check whether file and issue are in sync:

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

FILE_TITLE=$(python3 -c "
import re
m = re.search(r'^title:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else '')
")

ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --json title --jq '.title')

if [ "$FILE_TITLE" != "$ISSUE_TITLE" ]; then
  echo "[story-sync] DRIFT  title mismatch: file='$FILE_TITLE'  issue='$ISSUE_TITLE'"
  # File is authoritative for title — update the issue
  gh issue edit "$ISSUE_NUMBER" --title "$FILE_TITLE"
fi

Batch Sync (all stories)

find docs/stories -name "*.md" ! -name "_template.md" | while read -r f; do
  NUMBER=$(python3 -c "
import re
m = re.search(r'^issue_number:\s*(\d+)', open('$f').read(), re.MULTILINE)
print(m.group(1) if m else '')
")
  if [ -z "$NUMBER" ]; then
    echo "[story-sync] NEEDS_CREATE  $f"
  else
    echo "[story-sync] HAS_ISSUE    $f  → #$NUMBER"
  fi
done

Failure Modes

Condition Action
Story has issue_number: null Create the issue. Never skip.
Issue number in file but issue is closed Log warning. Do not reopen automatically — escalate to human.
Title drift detected File wins. Update issue title.
Labels on issue unknown to label set Log: [story-sync] UNKNOWN_LABEL {name}. Do not remove.
Story file missing after issue exists Log warning. Issue is not deleted. Human decides.

Logging

[story-sync] CREATED       #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] BODY_UPDATE   #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] LABELS_SYNC   #42  → docs/stories/user-auth-reset-0001.md
[story-sync] DRIFT         #42  title mismatch — issue updated
[story-sync] SKIP          #42  no changes detected
提供Stripe API集成最佳实践,涵盖Webhook安全验证、Checkout会话创建及幂等性处理。包含本地测试命令、TypeScript代码示例及安全规范,指导开发者正确接入支付功能。
需要实现Stripe支付功能 配置Stripe Webhook接收器 处理Stripe订阅或一次性付款 解决支付相关幂等性问题
.claude/skills/stripe-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill stripe-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "stripe-patterns",
    "description": "Apply Stripe API integration patterns: webhooks, idempotency keys, and subscription handling. Use when integrating Stripe payments."
}

SKILL: Stripe Patterns

Local Testing Setup

# Listen for webhooks and forward to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger checkout.session.completed
stripe trigger customer.subscription.created

# Monitor logs
stripe logs tail

Webhook Handler Pattern

// ALWAYS verify webhook signatures — never skip
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get('stripe-signature')!;
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
  } catch (err) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }

  // Idempotency: check if event already processed
  // Store event.id in database, skip if duplicate

  switch (event.type) {
    case 'payment_intent.succeeded': {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      // Handle success
      break;
    }
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      // Provision access
      break;
    }
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return new Response(JSON.stringify({ received: true }), { status: 200 });
}

Checkout Session Pattern

const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  payment_method_types: ['card'],
  line_items: [{
    price: priceId,
    quantity: 1,
  }],
  success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${baseUrl}/cancel`,
  customer_email: user.email,
  metadata: {
    userId: user.id,  // link back to your user
  },
}, {
  idempotencyKey: `checkout-${user.id}-${priceId}`,  // ALWAYS use idempotency keys
});

Rules

  • ALWAYS verify webhook signatures with constructEvent().
  • ALWAYS use idempotency keys on write operations.
  • NEVER log raw card data or full payment method details.
  • Use restricted API keys with minimum required permissions per service.
  • Test every webhook handler with stripe trigger.
  • Store Stripe customer IDs in your database for lookup.
提供 Supabase 开发最佳实践,涵盖本地工作流、SQL 迁移模板及 Edge Function 安全模式。强调启用 RLS、禁止暴露 service_role 密钥、使用 TypeScript 类型生成及追加式迁移规范,确保应用安全与类型安全。
构建 Supabase 数据库架构 编写 Supabase 行级安全策略 部署或调试 Supabase Edge Functions 需要 Supabase 本地开发环境配置
.claude/skills/supabase-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill supabase-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "supabase-patterns",
    "description": "Apply Supabase schema, RLS policies, and edge function patterns. Use when building on Supabase."
}

SKILL: Supabase Patterns

Local Development Workflow

# Start local Supabase stack
supabase start

# Apply migrations
supabase db push

# Generate TypeScript types
supabase gen types typescript --local > src/types/supabase.ts

# Deploy Edge Functions
supabase functions deploy {function-name}

# Stop
supabase stop

Migration Pattern

-- supabase/migrations/{timestamp}_{description}.sql
-- Up migration (always additive in production)

CREATE TABLE IF NOT EXISTS public.{table_name} (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  created_at timestamptz DEFAULT now() NOT NULL,
  updated_at timestamptz DEFAULT now() NOT NULL
);

-- Enable RLS ALWAYS
ALTER TABLE public.{table_name} ENABLE ROW LEVEL SECURITY;

-- RLS Policies
CREATE POLICY "{table_name}_select_own"
  ON public.{table_name} FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "{table_name}_insert_own"
  ON public.{table_name} FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "{table_name}_update_own"
  ON public.{table_name} FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "{table_name}_delete_own"
  ON public.{table_name} FOR DELETE
  USING (auth.uid() = user_id);

Edge Function Pattern

// supabase/functions/{name}/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req: Request) => {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' }
    })
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    { global: { headers: { Authorization: authHeader } } }
  )

  // Never use service_role key in Edge Functions unless absolutely necessary
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
  }

  // ... handler logic
})

Rules

  • Enable RLS on EVERY table with user data. No exceptions.
  • Never expose service_role key to client code.
  • Always use supabase gen types typescript for type safety.
  • Migrations are append-only (never modify existing migration files).
  • Test with supabase start before deploying.
遵循红绿重构TDD循环,先写失败测试再实现最小代码,最后重构。支持ATDD模式及Docker集成测试容器生命周期管理,强制测试前置、禁止弱断言和过度Mock,确保开发质量与回归安全。
实现新功能 编写单元测试或E2E测试 执行代码重构
.claude/skills/tdd-workflow/SKILL.md
npx skills add kinncj/Heimdall --skill tdd-workflow -g -y
SKILL.md
Frontmatter
{
    "name": "tdd-workflow",
    "description": "Drive development with a red-green-refactor TDD cycle, ensuring tests are written before implementation. Use when implementing any new functionality."
}

SKILL: TDD Workflow

The RED → GREEN → REFACTOR Cycle

RED Phase (QA Agent)

  1. Read the acceptance criterion or task description.
  2. Write a test that will fail because the implementation doesn't exist.
  3. Run the test. It MUST fail with a meaningful error (not a syntax error).
  4. If the test passes immediately → the test is wrong. Rewrite it.
  5. Report: file path, test name, failure message.

GREEN Phase (Specialist Agent)

  1. Read the failing test file at the provided path.
  2. Implement the MINIMUM code to make ONLY that test pass.
  3. Do not implement anything not required by the test.
  4. Run the test. It must pass.
  5. Run the full unit suite to verify no regressions.
  6. If the test fails after 3 attempts → escalate to Orchestrator.

REFACTOR Phase (Specialist Agent)

  1. Look for: duplication, poor naming, long functions, complex conditionals.
  2. Clean up without changing behavior.
  3. Run the test again. It must still pass.
  4. Commit: git commit -m "refactor: {what was cleaned up}"

ATDD Pattern (Acceptance Test-Driven Development)

  1. Write the E2E/acceptance test from the acceptance criterion (Given/When/Then).
  2. Watch it fail (RED).
  3. Drive out unit tests and implementation to make it pass (GREEN).
  4. Acceptance test goes green last.

Integration Test Container Lifecycle

# Before integration tests
docker compose -f docker-compose.test.yml up -d --wait

# Run integration tests
make test-integration

# After tests
docker compose -f docker-compose.test.yml down -v

Rules

  • Test file is created BEFORE implementation file.
  • Test name format: "should {expected outcome} when {condition}".
  • Never weaken an assertion to make a test pass.
  • Never mock what you can test with a real dependency (use containers).
  • Implementation agent receives FILE PATH, not requirement text.
  • Gate: test must fail before implementation, pass after.
提供Terraform基础设施即代码的最佳实践,涵盖模块化目录结构、资源定义规范及工作流。强制执行验证、计划及安全策略,强调FinOps成本注释与环境隔离,确保IaC的可维护性与安全性。
编写或重构Terraform代码 设计基础设施模块结构 配置状态管理与变量模式 执行Terraform工作流
.claude/skills/terraform-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill terraform-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "terraform-patterns",
    "description": "Apply Terraform module structure, state management, and variable patterns for infrastructure as code. Use when writing Terraform."
}

SKILL: Terraform Patterns

Module Structure

infra/terraform/
├── modules/
│   ├── networking/
│   ├── database/
│   └── compute/
└── environments/
    ├── dev/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── terraform.tfvars
    ├── staging/
    └── prod/

Module Pattern

# modules/database/main.tf
resource "aws_db_instance" "main" {
  # finops: ~$180/mo (db.t3.medium, 100GB gp3, single AZ)
  identifier     = "${var.environment}-${var.name}-db"
  engine         = "postgres"
  engine_version = "16"
  instance_class = var.instance_class

  allocated_storage     = var.storage_gb
  storage_type          = "gp3"
  storage_encrypted     = true

  username = var.username
  password = var.password

  vpc_security_group_ids = [aws_security_group.db.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  backup_retention_period = var.environment == "prod" ? 7 : 1
  deletion_protection     = var.environment == "prod"

  tags = merge(var.tags, {
    Environment = var.environment
    ManagedBy   = "terraform"
  })
}

Workflow

# Initialize (first time or after provider changes)
terraform init

# Format check (CI)
terraform fmt -check -recursive

# Validate syntax
terraform validate

# Plan (always before apply)
terraform plan -out=plan.tfplan -var-file=environments/dev/terraform.tfvars

# Apply (requires explicit instruction)
terraform apply plan.tfplan

# State inspection
terraform show
terraform state list

Rules

  • ALWAYS run terraform validate before reporting complete.
  • ALWAYS run terraform plan to show what will change.
  • NEVER run terraform apply without explicit instruction.
  • ALWAYS annotate resources with # finops: cost estimate.
  • Use workspaces or separate state files per environment.
  • Enable state locking (S3 + DynamoDB or Terraform Cloud).
  • Tag all resources with environment, managed-by, and cost-center.
基于STRIDE框架对组件和信任边界进行威胁建模,生成包含资产、风险分析及缓解措施的威胁登记册。适用于新功能安全评审,输出至指定文档路径,确保上线前关键风险得到控制。
需要对新功能进行安全评估 执行STRIDE威胁建模分析 生成威胁登记册
.claude/skills/threat-modeling/SKILL.md
npx skills add kinncj/Heimdall --skill threat-modeling -g -y
SKILL.md
Frontmatter
{
    "name": "threat-modeling",
    "description": "Perform STRIDE threat modeling for each component and trust boundary and produce a threat register. Use when reviewing a feature for security."
}

SKILL: Threat Modeling (STRIDE)

Output Location

docs/specs/{feature-slug}/threat-model.md

STRIDE Framework

For each component and trust boundary, evaluate all 6 threat categories:

Letter Threat Question
S Spoofing Can an attacker impersonate a user, service, or system?
T Tampering Can data be modified in transit or at rest without detection?
R Repudiation Can a user deny performing an action?
I Information Disclosure Can sensitive data leak to unauthorized parties?
D Denial of Service Can an attacker prevent legitimate users from accessing the service?
E Elevation of Privilege Can an attacker gain capabilities beyond what is authorized?

Threat Model Template

# Threat Model: {Feature Name}

## Assets
| Asset | Sensitivity | Owner |
|-------|-------------|-------|
| User PII | High | {team} |
| Payment data | Critical | {team} |
| API keys | Critical | {team} |

## Trust Boundaries
- Internet <-> Load Balancer
- Load Balancer <-> Application
- Application <-> Database
- Application <-> Third-party APIs

## STRIDE Analysis

### {Component Name}

**S — Spoofing**
- Risk: {description}
- Likelihood: High/Medium/Low
- Impact: High/Medium/Low
- Mitigation: {control}

**T — Tampering**
...

**R — Repudiation**
...

**I — Information Disclosure**
...

**D — Denial of Service**
...

**E — Elevation of Privilege**
...

## Risk Register
| Threat | Component | Likelihood | Impact | Risk Level | Mitigation | Status |
|--------|-----------|------------|--------|------------|------------|--------|
| SQL Injection | API | High | Critical | Critical | Parameterized queries | Mitigated |

## Mitigations Required Before Launch
- [ ] {Critical mitigation 1}
- [ ] {Critical mitigation 2}

Common Mitigations

  • Authentication: JWT with short expiry, refresh token rotation.
  • Authorization: RBAC, RLS on database.
  • Input validation: Zod/FluentValidation on all inputs.
  • Rate limiting: Per-IP and per-user limits.
  • Encryption: TLS 1.3 in transit, AES-256 at rest.
  • Audit logging: All write operations logged with actor + timestamp.
  • Secrets management: Never in code; use environment variables or vault.
提供 Vercel 部署、Edge 函数及环境变量配置的最佳实践。涵盖 vercel.json 设置、运行时选择(Edge/Node)、环境管理命令、构建部署流程及中间件模式,旨在优化 Vercel 平台的开发与发布效率。
需要配置 Vercel 部署文件 选择 Next.js 运行时类型 管理 Vercel 环境变量 执行 Vercel 本地构建或部署 编写 Vercel Edge 中间件
.claude/skills/vercel-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill vercel-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "vercel-patterns",
    "description": "Apply Vercel deployment, edge function, and environment variable patterns. Use when deploying to Vercel."
}

SKILL: Vercel Patterns

vercel.json Configuration

{
  "framework": "nextjs",
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm ci",
  "regions": ["iad1"],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Strict-Transport-Security", "value": "max-age=31536000" }
      ]
    }
  ],
  "rewrites": [],
  "redirects": [
    {
      "source": "/old-path",
      "destination": "/new-path",
      "permanent": true
    }
  ]
}

Runtime Selection

// Edge Runtime (low-latency, streaming, middleware)
export const runtime = 'edge';

// Node.js Runtime (heavy compute, native modules, file system)
export const runtime = 'nodejs';

// ISR (static with revalidation)
export const revalidate = 3600; // seconds

Environment Variables

# Pull env vars for local development
vercel env pull .env.local

# Add env var
vercel env add SECRET_KEY production

# List env vars
vercel env ls

Deployment Workflow

# Build locally first (catch errors before deploy)
vercel build

# Deploy prebuilt (faster, recommended for CI)
vercel deploy --prebuilt

# Deploy to production
vercel deploy --prod --prebuilt

Middleware Pattern

// middleware.ts (runs at edge, before every request)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Auth check, rate limiting, A/B testing, etc.
  const token = request.cookies.get('token');
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Rules

  • Run vercel build locally before deploying to catch errors.
  • Use vercel env pull to sync environment variables locally.
  • Prefer Edge runtime for middleware and simple API routes.
  • Use Node.js runtime for heavy compute or native module requirements.
  • Set security headers for all API routes.
根据品牌简报生成视觉识别规范(配色、排版、间距),输出为JSON文件供design-tokens使用。支持Web及终端(TUI)适配,确保WCAG对比度合规,需人工审批后生效。
建立新品牌视觉风格 需要生成设计令牌(JSON) 品牌关键词或描述输入
.claude/skills/visual-identity/SKILL.md
npx skills add kinncj/Heimdall --skill visual-identity -g -y
SKILL.md
Frontmatter
{
    "name": "visual-identity",
    "description": "Produce a coherent visual identity (palette, typography, spacing, radius) from a brief and serialise to JSON for the design-tokens skill. Use when establishing brand identity."
}

SKILL: visual-identity

Purpose

Produce a coherent visual identity from a brief or set of brand keywords. Outputs are palette, typography pairing, spacing scale, and radius scale — all serialised to JSON files that feed the design-tokens skill. Human approval required before tokens propagate to UI.

Inputs

Field Source Example
brief free-text brand description "modern fintech, trustworthy, minimal"
keywords 3–6 brand keywords ["trust", "clarity", "speed"]
primary_color optional hex override "#2563eb"
existing_tokens path to existing tokens.json docs/design/identity/tokens.json

Outputs

File Location Description
palette.json docs/design/identity/ All color roles with hex + WCAG contrast
typography.json docs/design/identity/ Font families, scale, weights, line-heights
tokens.json docs/design/identity/ W3C DTCG format — canonical token file

Target awareness

When design.target (project.config.yaml, default web) is tui, keep the same JSON outputs but constrain choices to the terminal: colors must be expressible in ANSI-256 (record the nearest 256 index alongside each hex), every fg/bg role pair must clear WCAG 2.2 AA contrast, and typography.json describes terminal text styles (bold, dim/faint, italic, underline, reverse) rather than font families and px sizes. The palette/typography/scale templates below still apply structurally; adapt their values for the terminal.

Palette Structure

docs/design/identity/palette.json:

{
  "brand": {
    "primary":   { "value": "#2563eb", "on": "#ffffff", "contrast_aa": true },
    "secondary": { "value": "#7c3aed", "on": "#ffffff", "contrast_aa": true },
    "accent":    { "value": "#0ea5e9", "on": "#ffffff", "contrast_aa": true }
  },
  "semantic": {
    "success":  { "value": "#16a34a", "on": "#ffffff", "contrast_aa": true },
    "warning":  { "value": "#d97706", "on": "#000000", "contrast_aa": true },
    "error":    { "value": "#dc2626", "on": "#ffffff", "contrast_aa": true },
    "info":     { "value": "#0284c7", "on": "#ffffff", "contrast_aa": true }
  },
  "neutral": {
    "50":  "#f8fafc",
    "100": "#f1f5f9",
    "200": "#e2e8f0",
    "300": "#cbd5e1",
    "400": "#94a3b8",
    "500": "#64748b",
    "600": "#475569",
    "700": "#334155",
    "800": "#1e293b",
    "900": "#0f172a"
  },
  "surface": {
    "background": "#ffffff",
    "foreground": "#0f172a",
    "muted":      "#f1f5f9",
    "border":     "#e2e8f0"
  }
}

WCAG Contrast Check

All palette entries with on values must pass WCAG 2.2 AA (4.5:1 for normal text, 3:1 for large text):

def relative_luminance(hex_color):
    r, g, b = (int(hex_color[i:i+2], 16) / 255 for i in (1, 3, 5))
    def linearize(c):
        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    r, g, b = linearize(r), linearize(g), linearize(b)
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def contrast_ratio(fg, bg):
    l1 = relative_luminance(fg)
    l2 = relative_luminance(bg)
    lighter, darker = max(l1, l2), min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

# AA normal text requires >= 4.5
# AA large text requires >= 3.0
ratio = contrast_ratio("#2563eb", "#ffffff")
assert ratio >= 4.5, f"FAIL: contrast {ratio:.2f} < 4.5 for #2563eb on #ffffff"

If any pair fails AA, adjust the shade until it passes. Never ship a palette with failing contrast.

Typography Structure

docs/design/identity/typography.json:

{
  "families": {
    "sans":  "Inter, system-ui, -apple-system, sans-serif",
    "mono":  "JetBrains Mono, 'Fira Code', monospace",
    "serif": null
  },
  "scale": {
    "xs":   { "size": "0.75rem",  "line": "1rem" },
    "sm":   { "size": "0.875rem", "line": "1.25rem" },
    "base": { "size": "1rem",     "line": "1.5rem" },
    "lg":   { "size": "1.125rem", "line": "1.75rem" },
    "xl":   { "size": "1.25rem",  "line": "1.75rem" },
    "2xl":  { "size": "1.5rem",   "line": "2rem" },
    "3xl":  { "size": "1.875rem", "line": "2.25rem" },
    "4xl":  { "size": "2.25rem",  "line": "2.5rem" }
  },
  "weights": {
    "regular": 400,
    "medium":  500,
    "semibold": 600,
    "bold":    700
  }
}

Spacing + Radius Scales

Written directly into tokens.json (see design-tokens skill for full schema):

{
  "spacing": {
    "1":  "0.25rem",
    "2":  "0.5rem",
    "3":  "0.75rem",
    "4":  "1rem",
    "6":  "1.5rem",
    "8":  "2rem",
    "12": "3rem",
    "16": "4rem"
  },
  "radius": {
    "none": "0",
    "sm":   "0.125rem",
    "md":   "0.375rem",
    "lg":   "0.5rem",
    "xl":   "0.75rem",
    "full": "9999px"
  }
}

Write palette.json

mkdir -p docs/design/identity
# Write palette.json from the template above, populated with chosen values
# Then validate all contrast ratios before writing
python3 scripts/validate-palette.py docs/design/identity/palette.json

Approval Gate

Mark docs/design/identity/palette.json approved before design-tokens runs:

Add to the top of palette.json:

{ "_meta": { "status": "draft", "approved_by": null, "approved_at": null }, ... }

Check in design-tokens skill:

STATUS=$(python3 -c "import json; print(json.load(open('docs/design/identity/palette.json'))['_meta']['status'])")
[ "$STATUS" = "approved" ] || { echo "BLOCKED: palette not approved"; exit 1; }

Failure Modes

Condition Action
Contrast ratio fails AA Adjust shade. Re-check. Never skip.
existing_tokens provided Merge: only fill missing roles; do not overwrite approved values.
No brief or keywords provided Generate a neutral/professional default palette. Log DEFAULT_PALETTE.
palette.json exists and is approved Do not overwrite. Log SKIP — approved identity exists.

Logging

[visual-identity] PALETTE    docs/design/identity/palette.json  contrast=all-pass
[visual-identity] TYPOGRAPHY docs/design/identity/typography.json
[visual-identity] SKIP       docs/design/identity/palette.json  (approved — locked)
[visual-identity] CONTRAST_FAIL  #2563eb on #ffffff  ratio=3.8  required=4.5
根据用户故事文件生成低保真线框图,支持ASCII、SVG或HTML格式。自动适配Web和TUI目标,确保输出完整性并需人工审批后方可进入后续设计阶段。
需要为UI故事创建低保真线框图 从用户故事文件生成界面布局草图
.claude/skills/wireframe/SKILL.md
npx skills add kinncj/Heimdall --skill wireframe -g -y
SKILL.md
Frontmatter
{
    "name": "wireframe",
    "description": "Generate low-fidelity wireframes (ASCII, SVG, or HTML) from user story files. Use when creating wireframes for UI stories."
}

SKILL: wireframe

Purpose

Generate low-fidelity wireframes from user story files. Output is deterministic — given the same story and layout hints, the same wireframe structure is produced. Three output formats: ASCII (default), SVG, HTML. Human approval is required before the wireframe feeds downstream mockup work.

Inputs

Field Source Example
story_file path to story markdown docs/stories/auth-reset-0001.md
ui_components derived from story Gherkin form, button, error message
stack project.config.yaml react-mantine

Outputs

The required files depend on design.target in project.config.yaml (default web) — see Target awareness:

File Location Targets
<story-id>.wireframe.md docs/design/wireframes/ web + tui — ASCII layout + approval metadata
<story-id>.wireframe.excalidraw docs/design/wireframes/ web + tui — editable Excalidraw diagram
<story-id>.wireframe.html docs/design/wireframes/ web only — browser-previewable static wireframe

A wireframe stage that produces fewer than the files required for the active target is incomplete. Do not PAUSE or mark DONE without them.

Target awareness

design.target decides which files are required:

  • web<id>.wireframe.md (ASCII) + .html (preview) + .excalidraw.
  • tui<id>.wireframe.md (ASCII/box-drawing) + .excalidraw. No .html.

Use box-drawing primitives for tui layouts, label panes/overlays/status bar, and include the keybinding legend and focus order inline in the .md. The Excalidraw generator below applies to both targets; the HTML generator is web only.

ASCII Wireframe Primitives

Use these consistently across all wireframes:

┌─────────────────────────────────┐   ← container / card
│  [Label]  [Input Field      ]   │   ← label + text input
│  [Button: Primary Action    ]   │   ← primary button
│  [Button: Secondary]            │   ← secondary button
│  ○ Option A  ○ Option B         │   ← radio group
│  ☐ Checkbox label               │   ← checkbox
│  ▼ Dropdown / Select            │   ← select / combobox
│  ──────────────────────         │   ← divider
│  ⚠ Error message text           │   ← validation error
│  ✓ Success confirmation         │   ← success state
└─────────────────────────────────┘

[Nav: Logo | Item 1 | Item 2 | CTA]  ← navigation bar
[ Sidebar  ][     Main Content    ]  ← two-column layout
[  Col 1  ][  Col 2  ][  Col 3  ]   ← three-column grid
[         Full-width Banner         ]← hero / header band

ASCII Wireframe — Example (Password Reset)

┌──────────────────────────────────────┐
│            Reset Password            │
│                                      │
│  Email                               │
│  [                              ]    │
│                                      │
│  ⚠ No account found for this email  │  ← error state
│                                      │
│  [Button: Send Reset Link       ]    │
│                                      │
│  ← Back to Login                     │
└──────────────────────────────────────┘

Generate Wireframe Files

For each story, produce all three output files. Run these steps:

STORY_FILE="docs/stories/auth-reset-password-20250416143000-0001.md"
STORY_ID=$(python3 -c "
import re
m = re.search(r'^id:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
mkdir -p docs/design/wireframes

Step 1 — Write ${STORY_ID}.wireframe.md (ASCII layout + approval metadata):

---
story_id: "{story_id}"
story_file: "{story_file}"
status: draft          # draft | approved | rejected
approved_by: null
approved_at: null
---

## Wireframe: {story title}

### Default state
{ASCII wireframe}

### Error state
{ASCII wireframe — validation error}

### Success state
{ASCII wireframe — confirmation}

### Interaction Notes

- Tab order: {list of focusable elements in tab sequence}
- Primary action: {describe}
- Error handling: {describe visible error states}

### Approval

- [ ] Approved by product owner
- [ ] Approved by UX lead (if applicable)

Step 2 — Write ${STORY_ID}.wireframe.html (see HTML template below)

Step 3 — Write ${STORY_ID}.wireframe.excalidraw (see Excalidraw template below)

After writing the files required for the active target, verify they exist (web: md, html, excalidraw; tui: md, excalidraw):

ls docs/design/wireframes/${STORY_ID}.wireframe.md
ls docs/design/wireframes/${STORY_ID}.wireframe.excalidraw
# web target also:
ls docs/design/wireframes/${STORY_ID}.wireframe.html 2>/dev/null || true

If any required file is missing, produce it before continuing.

HTML Wireframe (web target only)

For web targets, always generate the HTML wireframe. Skip this section entirely for tui. Use multiple <section> blocks for multi-state wireframes (default, loading, error, success, empty).

cat > "docs/design/wireframes/${STORY_ID}.wireframe.html" <<'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Wireframe: {story title}</title>
  <style>
    * { box-sizing: border-box; font-family: monospace; }
    body { background: #e9ecef; padding: 2rem; }
    h1 { font-size: 1rem; color: #495057; margin-bottom: 1.5rem; }
    .states { display: flex; flex-wrap: wrap; gap: 1.5rem; }
    .state { background: #f8f9fa; border: 1px solid #adb5bd; border-radius: 4px; padding: 0; min-width: 360px; }
    .state-label { background: #343a40; color: #fff; font-size: .75rem; padding: .25rem .75rem; border-radius: 4px 4px 0 0; }
    .frame { padding: 1.5rem; }
    .screen-title { font-weight: bold; font-size: 1.1rem; margin-bottom: 1rem; border-bottom: 1px solid #dee2e6; padding-bottom: .5rem; }
    label { display: block; font-size: .8rem; color: #495057; margin-bottom: .2rem; margin-top: .75rem; }
    .input { border: 1px solid #868e96; padding: .4rem .6rem; width: 100%; background: #fff; }
    .btn { border: none; padding: .5rem 1rem; cursor: default; margin-top: .75rem; width: 100%; font-weight: bold; }
    .btn-primary { background: #343a40; color: #fff; }
    .btn-secondary { background: transparent; border: 1px solid #343a40; color: #343a40; }
    .error { color: #c0392b; font-size: .8rem; margin-top: .25rem; }
    .success { color: #2d6a4f; font-size: .8rem; margin-top: .25rem; }
    .link { color: #1971c2; font-size: .85rem; margin-top: .75rem; display: block; }
    .nav { display: flex; gap: 1rem; background: #343a40; color: #fff; padding: .6rem 1rem; font-size: .85rem; margin-bottom: .75rem; }
    .badge { background: #868e96; color: #fff; font-size: .7rem; padding: .1rem .4rem; border-radius: 3px; }
    .divider { border: none; border-top: 1px solid #dee2e6; margin: .75rem 0; }
    .tab-order { font-size: .7rem; color: #868e96; margin-top: 1.5rem; border-top: 1px dashed #dee2e6; padding-top: .5rem; }
  </style>
</head>
<body>
  <h1>Wireframe: {story title} — {story_id}</h1>
  <div class="states">

    <div class="state">
      <div class="state-label">Default state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <!-- Add form fields, buttons, content blocks here -->
        <label>Field label</label>
        <input class="input" type="text" placeholder="placeholder" disabled />
        <div class="btn btn-primary">Primary Action</div>
        <a class="link" href="#">Secondary link</a>
        <div class="tab-order">Tab order: Field → Primary Action → Secondary link</div>
      </div>
    </div>

    <div class="state">
      <div class="state-label">Error state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <label>Field label</label>
        <input class="input" type="text" placeholder="placeholder" disabled style="border-color:#c0392b" />
        <div class="error">⚠ Error message describing the problem</div>
        <div class="btn btn-primary">Primary Action</div>
        <div class="tab-order">Tab order: Field → Primary Action</div>
      </div>
    </div>

    <div class="state">
      <div class="state-label">Success state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <div class="success">✓ Success confirmation message</div>
        <div class="btn btn-secondary">Back / Next step</div>
      </div>
    </div>

  </div>
</body>
</html>
HTMLEOF

Extend with real field names, content, and states from the story Gherkin. One <div class="state"> block per Gherkin scenario.

Excalidraw Wireframe (always required)

Always generate the Excalidraw file — every story, every run. Excalidraw is the canonical editable wireframe format reviewers annotate.

Write the file as valid JSON to docs/design/wireframes/${STORY_ID}.wireframe.excalidraw. Each UI element is one entry in the elements array. Use the element templates below, copy and adapt:

python3 - <<'PYEOF'
import json, pathlib, os

story_id = os.environ.get("STORY_ID", "unknown")
out = pathlib.Path(f"docs/design/wireframes/{story_id}.wireframe.excalidraw")
out.parent.mkdir(parents=True, exist_ok=True)

# ── Element helpers ──────────────────────────────────────────────────────────
def rect(id, x, y, w, h, label="", bg="transparent", stroke="#343a40", bold=False):
    els = [{
        "id": id, "type": "rectangle", "x": x, "y": y, "width": w, "height": h,
        "angle": 0, "strokeColor": stroke, "backgroundColor": bg,
        "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid",
        "roughness": 1, "opacity": 100, "groupIds": [], "roundness": {"type": 3},
        "version": 1, "versionNonce": 1, "isDeleted": False,
        "boundElements": None, "updated": 1, "link": None, "locked": False,
    }]
    if label:
        els.append(text(id + "_lbl", x + w/2, y + h/2, label, bold=bold, anchor="center"))
    return els

def text(id, x, y, content, bold=False, anchor="left", color="#343a40"):
    return {
        "id": id, "type": "text", "x": x, "y": y,
        "width": len(content) * 8, "height": 20,
        "angle": 0, "strokeColor": color, "backgroundColor": "transparent",
        "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid",
        "roughness": 1, "opacity": 100, "groupIds": [], "roundness": None,
        "version": 1, "versionNonce": 1, "isDeleted": False,
        "boundElements": None, "updated": 1, "link": None, "locked": False,
        "text": content, "fontSize": 16,
        "fontFamily": 3,  # monospace
        "textAlign": anchor, "verticalAlign": "middle",
        "baseline": 14, "containerId": None, "originalText": content,
        "lineHeight": 1.25,
        "fontWeight": "bold" if bold else "normal",
    }

def input_field(id, x, y, w, label_text):
    return (
        [text(id + "_lbl", x, y - 18, label_text)] +
        rect(id, x, y, w, 32, stroke="#868e96")
    )

def button_primary(id, x, y, w, label_text):
    return rect(id, x, y, w, 36, label=label_text, bg="#343a40", stroke="#343a40", bold=True)

def button_secondary(id, x, y, w, label_text):
    return rect(id, x, y, w, 36, label=label_text, bg="transparent", stroke="#343a40")

def section_label(id, x, y, content):
    return [text(id, x, y, f"[ {content} ]", color="#868e96")]

# ── Build elements ────────────────────────────────────────────────────────────
elements = []

# Outer frame
elements += rect("frame", 50, 30, 700, 600, stroke="#343a40")

# Screen title
elements.append(text("title", 70, 50, "{Screen Title}", bold=True))

# --- Default state ---
elements += section_label("s_default", 70, 90, "Default state")
elements += input_field("field1", 70, 130, 560, "Field label")
elements += button_primary("btn_primary", 70, 190, 560, "Primary Action")
elements.append(text("link1", 70, 238, "← Secondary link / back", color="#1971c2"))

# --- Error state ---
elements += section_label("s_error", 70, 280, "Error state")
elements += input_field("field1_err", 70, 320, 560, "Field label")
elements += rect("err_border", 70, 320, 560, 32, stroke="#c0392b")
elements.append(text("err_msg", 70, 360, "⚠ Error message text", color="#c0392b"))
elements += button_primary("btn_primary_err", 70, 390, 560, "Primary Action")

# --- Success state ---
elements += section_label("s_success", 70, 450, "Success state")
elements.append(text("success_msg", 70, 490, "✓ Success confirmation", color="#2d6a4f"))
elements += button_secondary("btn_back", 70, 520, 260, "Back / Next step")

doc = {
    "type": "excalidraw",
    "version": 2,
    "source": "MAPLE wireframe-architect",
    "elements": elements,
    "appState": {"viewBackgroundColor": "#f8f9fa", "gridSize": None},
    "files": {},
}
out.write_text(json.dumps(doc, indent=2) + "\n")
print(f"[wireframe] wrote {out}")
PYEOF

Adapt element positions and labels to match the actual story screens. Add more rect/text/input_field/button_primary calls per Gherkin scenario. Do not leave placeholder text ({Screen Title}) in the final file.

Approval Gate

All three files must exist and the .md must be status: approved before mockup or ui-mockup-builder proceeds:

python3 - <<'EOF'
import re, sys, pathlib
sid = open(".claude/state/maple.json").read()  # or pass STORY_ID
# Check required artifacts exist (html is web-only)
cfg = open("project.config.yaml").read() if pathlib.Path("project.config.yaml").exists() else ""
tm = re.search(r'^\s*target:\s*(\w+)', cfg, re.MULTILINE)
target = tm.group(1) if tm else "web"
exts = ["md", "excalidraw"] if target == "tui" else ["md", "html", "excalidraw"]
for ext in exts:
    p = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.{ext}")
    if not p.exists():
        print(f"BLOCKED: missing {p}")
        sys.exit(1)
# Check approval status in .md
md = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.md").read_text()
m = re.search(r'^status:\s*(\w+)', md, re.MULTILINE)
status = m.group(1) if m else 'draft'
if status != 'approved':
    print(f"BLOCKED: wireframe {sid} not approved (status={status})")
    sys.exit(1)
print("approved — all three artifacts present")
EOF

Failure Modes

Condition Action
Story has no Gherkin Generate skeleton wireframe with placeholder states. Log NO_GHERKIN — skeleton only.
docs/design/wireframes/ missing Create it.
Wireframe .md exists and is approved Do not overwrite. Log SKIP — approved wireframe exists.
Wireframe .md exists and is draft Overwrite only if story Gherkin has changed.
.html or .excalidraw missing despite .md existing Generate the missing file(s) immediately.

Logging

[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.md
[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.html
[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.excalidraw
[wireframe] SKIP     docs/design/wireframes/auth-reset-0001.wireframe.md  (approved — locked)
[wireframe] BLOCKED  auth-reset-0001  status=draft — needs approval before mockup
[wireframe] BLOCKED  auth-reset-0001  missing .html — generating now
对生成的UI进行WCAG 2.2 AA级无障碍审计,支持axe-core或pa11y工具,适用于含ui:true的Story。检测键盘可达性、对比度等违规项,生成JSON报告并作为PR评论,阻断合并。针对tui目标使用终端检查清单。
故事包含 ui:true 属性 用户明确要求执行无障碍审计
.cursor/skills/a11y-audit/SKILL.md
npx skills add kinncj/Heimdall --skill a11y-audit -g -y
SKILL.md
Frontmatter
{
    "name": "a11y-audit",
    "description": "Run WCAG 2.2 Level AA accessibility audits against generated UI. Use when a story has ui:true or when asked to audit accessibility."
}

SKILL: a11y-audit

Purpose

Run WCAG 2.2 Level AA accessibility audits against generated UI. Uses axe-core (via @axe-core/cli) or pa11y as available. Posts findings as PR comments. Blocks merge on AA violations. Required for every story with ui: true.

Target awareness

When design.target (project.config.yaml, default web) is tui, skip the browser tooling (axe/pa11y) and the preview URL. Instead evaluate the terminal a11y checklist from docs/design/design-targets.md and write the findings to docs/design/mockups/<id>.a11y.json using the SAME object shape the web path produces — an object with violations[], where each violation has id, impact (critical|serious|moderate|minor), description, help, and nodes[] ({target:[...], failureSummary}). The gate (scripts/sdlc/a11y-gate.sh) reads only violations[].impact, so matching this shape keeps it unchanged. Map checklist failures to impacts: keyboard-reachable→critical, focus-visible→serious, color-contrast→serious, color-only-signaling→serious, no-color-support→moderate, min-width-resize→moderate. The WCAG criteria, tool-detection, axe/pa11y, and PR-comment sections below apply to web targets.

WCAG 2.2 AA — Minimum Requirements

Criterion Level What to check
1.1.1 Non-text Content A All <img> have alt. Icons used as UI controls have labels.
1.3.1 Info and Relationships A Semantic HTML: headings, lists, tables, landmarks used correctly.
1.4.3 Contrast (minimum) AA Normal text ≥ 4.5:1. Large text ≥ 3:1.
1.4.11 Non-text Contrast AA UI components and focus indicators ≥ 3:1 against background.
2.1.1 Keyboard A All interactive elements reachable and operable via keyboard.
2.4.3 Focus Order A Tab order is logical and follows visual layout.
2.4.7 Focus Visible AA Focus indicator visible on all interactive elements.
3.3.1 Error Identification A Errors identified in text, not color alone.
3.3.2 Labels or Instructions A All inputs have labels.
4.1.2 Name, Role, Value A All UI components have accessible name, role, and state.

Tool Detection

detect_tool() {
  if command -v axe &>/dev/null; then
    echo "axe"
  elif command -v pa11y &>/dev/null; then
    echo "pa11y"
  elif npx --yes @axe-core/cli --version &>/dev/null 2>&1; then
    echo "axe-npx"
  else
    echo "none"
  fi
}
TOOL=$(detect_tool)

Run with axe-core CLI

URL="${1:-http://localhost:3000}"  # preview URL or Storybook story URL
STORY_ID="${2:-unknown}"
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

mkdir -p docs/design/mockups

axe "$URL" \
  --stdout \
  --tags wcag2a,wcag2aa,wcag21aa,wcag22aa \
  > "$REPORT"

VIOLATIONS=$(python3 -c "import json,sys; d=json.load(open('$REPORT')); print(len(d.get('violations',[])))")
echo "[a11y-audit] axe  $URL  violations=$VIOLATIONS"

Run with pa11y

URL="${1:-http://localhost:3000}"
STORY_ID="${2:-unknown}"
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

pa11y "$URL" \
  --standard WCAG2AA \
  --reporter json \
  > "$REPORT" 2>&1

ISSUES=$(python3 -c "import json,sys; d=json.load(open('$REPORT')); print(len(d) if isinstance(d,list) else 0)")
echo "[a11y-audit] pa11y  $URL  issues=$ISSUES"

Parse Results and Classify

import json, sys

report_path = sys.argv[1]
data = json.load(open(report_path))

# axe format
violations = data.get("violations", [])
passes     = data.get("passes", [])

critical = [v for v in violations if v["impact"] in ("critical", "serious")]
moderate = [v for v in violations if v["impact"] == "moderate"]
minor    = [v for v in violations if v["impact"] == "minor"]

print(f"CRITICAL/SERIOUS: {len(critical)}")
print(f"MODERATE:         {len(moderate)}")
print(f"MINOR:            {len(minor)}")
print(f"PASSES:           {len(passes)}")

# Merge gate: block on critical + serious
if critical:
    print("\nMERGE BLOCKED — resolve the following before merging:")
    for v in critical:
        print(f"  [{v['impact'].upper()}] {v['id']}: {v['description']}")
        for node in v.get("nodes", [])[:2]:
            print(f"    → {node.get('target', ['?'])[0]}: {node.get('failureSummary','')[:120]}")
    sys.exit(1)

Post Findings as PR Comment

STORY_ID="auth-reset-0001"
PR_NUMBER=$(gh pr list --head "$(git branch --show-current)" --json number --jq '.[0].number')
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

# Build comment body
COMMENT=$(python3 - <<'EOF'
import json, sys

data = json.load(open(sys.argv[1]))
violations = data.get("violations", [])

if not violations:
    print("## ✅ A11y Audit Passed\n\nNo WCAG 2.2 AA violations found.")
    sys.exit(0)

lines = [f"## ⚠️ A11y Audit: {len(violations)} violation(s) found\n"]
for v in violations[:10]:
    impact = v.get("impact","").upper()
    lines.append(f"### [{impact}] {v['id']}")
    lines.append(f"_{v['description']}_\n")
    for node in v.get("nodes",[])[:2]:
        selector = ', '.join(node.get("target",[]))
        lines.append(f"- `{selector}`")
        lines.append(f"  {node.get('failureSummary','')[:200]}")
    lines.append("")

if len(violations) > 10:
    lines.append(f"_...and {len(violations)-10} more. See full report in `docs/design/mockups/`._")

print('\n'.join(lines))
EOF
"$REPORT")

gh pr comment "$PR_NUMBER" --body "$COMMENT"
echo "[a11y-audit] PR_COMMENT  #$PR_NUMBER  violations=$(echo "$COMMENT" | grep -c '\[CRITICAL\]\|\[SERIOUS\]' || echo 0)"

Merge Gate Check

Called from scripts/sdlc/a11y-gate.sh (added in Phase VII):

STORY_FILE="$1"
UI=$(python3 -c "
import re
m = re.search(r'^ui:\s*(true|false)', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'false')
")

if [ "$UI" != "true" ]; then
  echo "[a11y-audit] SKIP  ui:false story — no audit required"
  exit 0
fi

STORY_ID=$(python3 -c "
import re
m = re.search(r'^id:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

if [ ! -f "$REPORT" ]; then
  echo "[a11y-audit] FAIL  no audit report found for $STORY_ID — run audit before merging"
  exit 1
fi

python3 scripts/sdlc/parse-a11y-report.py "$REPORT"

Manual Audit Checklist (no tool available)

When TOOL=none, perform a structured manual check:

## Manual A11y Checklist — {story_id}

### Perceivable
- [ ] All images have meaningful `alt` text (or `alt=""` for decorative)
- [ ] Color is not the sole means of conveying information
- [ ] Normal text contrast ≥ 4.5:1 (check with browser DevTools)
- [ ] Large text contrast ≥ 3:1

### Operable
- [ ] Tab through the entire form/component with keyboard only
- [ ] All actions reachable without mouse
- [ ] No keyboard trap
- [ ] Focus indicator visible on every interactive element

### Understandable
- [ ] Form inputs have visible labels (not just placeholders)
- [ ] Errors described in text, not color alone
- [ ] Error messages are specific and actionable

### Robust
- [ ] Correct semantic elements: `<button>`, `<input>`, `<label>`
- [ ] ARIA attributes only where native HTML is insufficient
- [ ] Works with browser zoom at 200%

Failure Modes

Condition Action
No tool available Fall back to manual checklist. Log TOOL=none — manual audit required.
Preview URL not reachable Log URL_UNREACHABLE. Do not skip audit — escalate.
ui: false story Skip audit. Log SKIP — ui:false.
Critical violations found Post PR comment. Exit non-zero. Block merge.
Report already exists and is current Skip re-run. Log SKIP — current report exists.

Logging

[a11y-audit] RUN      http://localhost:6006/... story=auth-reset-0001  tool=axe
[a11y-audit] RESULT   violations=0  passes=24  WCAG2AA=PASS
[a11y-audit] RESULT   violations=3  critical=1  WCAG2AA=FAIL
[a11y-audit] PR_COMMENT  #42  violations=1
[a11y-audit] SKIP     ui:false story — audit not required
[a11y-audit] BLOCKED  merge blocked — 1 critical violation unresolved
根据设计令牌、故事ID和堆栈配置,自动生成包含实现文件、Storybook故事、单元测试及Gherkin规范的完整UI组件骨架。
需要创建新的UI组件 初始化组件文件结构
.cursor/skills/component-scaffold/SKILL.md
npx skills add kinncj/Heimdall --skill component-scaffold -g -y
SKILL.md
Frontmatter
{
    "name": "component-scaffold",
    "description": "Generate a complete component file tree wired to design tokens with tests and Gherkin spec. Use when scaffolding a new UI component."
}

SKILL: component-scaffold

Purpose

Generate a complete component file tree wired to design tokens. Each component gets an implementation file, Storybook story, unit test, and Gherkin spec file. Skeletons are runnable with TODO stubs — not empty files. Stack is auto-detected from project.config.yaml.

Inputs

Field Source Example
component_name PascalCase PasswordResetForm
story_id story frontmatter id auth-reset-0001
tokens_file docs/design/identity/tokens.json required
stack project.config.yaml react-mantine | react-tailwind | html
mockup_file approved mockup docs/design/mockups/auth-reset-0001.mockup.tsx

Output Tree

For PasswordResetForm with stack react-mantine:

app/
└── components/
    └── PasswordResetForm/
        ├── index.tsx              ← component implementation
        ├── PasswordResetForm.stories.tsx  ← Storybook story
        ├── PasswordResetForm.test.tsx     ← unit tests (Vitest / Jest)
        └── PasswordResetForm.spec.ts      ← Gherkin step binding

Generate Component Index (react-mantine)

app/components/{ComponentName}/index.tsx:

// {ComponentName} — generated from story {story_id}
// Tokens: docs/design/identity/mantine.theme.ts
// TODO: implement from mockup docs/design/mockups/{story_id}.mockup.tsx

import { Stack, TextInput, Button } from '@mantine/core';
import { useState } from 'react';

export interface {ComponentName}Props {
  // TODO: define props from story acceptance criteria
  onSubmit?: (data: Record<string, unknown>) => void;
}

export function {ComponentName}({ onSubmit }: {ComponentName}Props) {
  // TODO: implement
  return (
    <Stack>
      {/* TODO: build from approved mockup */}
    </Stack>
  );
}

export default {ComponentName};

Generate Storybook Story

app/components/{ComponentName}/{ComponentName}.stories.tsx:

import type { Meta, StoryObj } from '@storybook/react';
import { {ComponentName} } from '.';

const meta: Meta<typeof {ComponentName}> = {
  title: 'Components/{ComponentName}',
  component: {ComponentName},
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof {ComponentName}>;

export const Default: Story = {
  args: {},
};

export const WithError: Story = {
  args: {
    // TODO: add error props
  },
};

Generate Unit Test

app/components/{ComponentName}/{ComponentName}.test.tsx:

import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { {ComponentName} } from '.';

describe('{ComponentName}', () => {
  it('renders without crashing', () => {
    render(<{ComponentName} />);
    // TODO: assert presence of key elements from wireframe
    expect(document.body).toBeTruthy();
  });

  it('calls onSubmit when form is submitted', () => {
    // TODO: implement interaction test
  });

  it('shows validation error when input is invalid', () => {
    // TODO: implement error state test
  });
});

Generate Gherkin Step Binding

app/components/{ComponentName}/{ComponentName}.spec.ts:

// Gherkin step bindings for story {story_id}
// Feature file: tests/features/{epic}/{story_slug}.feature
import { Given, When, Then } from '@cucumber/cucumber';

// TODO: copy matching steps from cucumber-automation output
// Example:
// Given('the user is on the {string} page', async (page: string) => {
//   throw new Error('Pending');
// });

Python Stack Alternative

For stack=python (e.g., Django / HTMX):

app/
└── components/
    └── password_reset_form/
        ├── __init__.py
        ├── template.html
        ├── test_password_reset_form.py
        └── password_reset_form_steps.py

Scaffold Script

COMPONENT="PasswordResetForm"
STORY_ID="auth-reset-0001"
STACK="react-mantine"
DIR="app/components/$COMPONENT"

mkdir -p "$DIR"

# Check for existing files — never overwrite
for f in "index.tsx" "${COMPONENT}.stories.tsx" "${COMPONENT}.test.tsx" "${COMPONENT}.spec.ts"; do
  if [ -f "$DIR/$f" ]; then
    echo "[component-scaffold] SKIP  $DIR/$f  (already exists)"
  else
    echo "[component-scaffold] CREATE  $DIR/$f"
    # write the appropriate template (see above)
  fi
done

Token Wiring

After scaffold, inject token import into index.tsx:

# Verify mantine.theme.ts exists
[ -f "docs/design/identity/mantine.theme.ts" ] || \
  echo "[component-scaffold] WARN  mantine.theme.ts missing — run design-tokens skill"

Failure Modes

Condition Action
ComponentName not PascalCase Warn and convert: password-reset-formPasswordResetForm.
app/components/ does not exist Create it. Log the creation.
File already exists Skip with SKIP log — never overwrite existing implementations.
tokens.json missing Scaffold without token import. Add // TODO: wire to design tokens comment.
Stack unknown Default to react-mantine. Log warning.

Logging

[component-scaffold] CREATE  app/components/PasswordResetForm/index.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.stories.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.test.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.spec.ts
[component-scaffold] SKIP    app/components/PasswordResetForm/index.tsx  (already exists)
[component-scaffold] WARN    mantine.theme.ts missing
从故事Markdown文件提取Gherkin场景生成可运行的.feature文件,并自动生成对应测试栈的Step Definition存根。支持TypeScript、JavaScript、Python和Java,保持测试与文档同步。
需要将故事文档同步为Cucumber测试用例 需要为新功能生成步骤定义存根
.cursor/skills/cucumber-automation/SKILL.md
npx skills add kinncj/Heimdall --skill cucumber-automation -g -y
SKILL.md
Frontmatter
{
    "name": "cucumber-automation",
    "description": "Extract Gherkin scenarios from story markdown files into runnable .feature files and generate step definition stubs. Use when syncing stories to test suites."
}

SKILL: cucumber-automation

Purpose

Extract Gherkin scenarios from story markdown files into runnable .feature files, and generate step definition stubs for the project's test stack. Keeps tests/features/ in sync with docs/stories/ as the single source of Gherkin truth.

Supported Stacks

Stack Step file extension Framework
TypeScript / Node.js .steps.ts @cucumber/cucumber
JavaScript / Node.js .steps.js @cucumber/cucumber
Python _steps.py behave
Java Steps.java Cucumber-JVM

Detect the stack from the repo root:

if [ -f "package.json" ] && grep -q "@cucumber/cucumber" package.json 2>/dev/null; then
  STACK="typescript"
elif [ -f "requirements.txt" ] && grep -q "behave" requirements.txt 2>/dev/null; then
  STACK="python"
elif [ -f "pom.xml" ] || [ -f "build.gradle" ]; then
  STACK="java"
else
  STACK="unknown"
fi

Extract Gherkin from a Story File

Story files contain Gherkin in fenced code blocks tagged with gherkin:

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"
EPIC="user-auth"

# Extract content of ```gherkin ... ``` blocks
python3 - <<'EOF'
import re, sys

story = open(sys.argv[1]).read()
blocks = re.findall(r'```gherkin\n(.*?)```', story, re.DOTALL)
if not blocks:
    print("NO_GHERKIN", file=sys.stderr)
    sys.exit(1)
print('\n\n'.join(blocks))
EOF "$STORY_FILE"

Write the Feature File

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"
FEATURE_DIR="tests/features"
EPIC="user-auth"

mkdir -p "$FEATURE_DIR/$EPIC"
FEATURE_FILE="$FEATURE_DIR/$EPIC/reset-password.feature"

# Extract and write (idempotent — overwrites if Gherkin changed)
python3 - <<'EOF'
import re, sys, os

story_path, feature_path = sys.argv[1], sys.argv[2]
story = open(story_path).read()
blocks = re.findall(r'```gherkin\n(.*?)```', story, re.DOTALL)
if not blocks:
    print(f"[cucumber] NO_GHERKIN in {story_path}", file=sys.stderr)
    sys.exit(1)

os.makedirs(os.path.dirname(feature_path), exist_ok=True)
with open(feature_path, 'w') as f:
    f.write('\n\n'.join(b.rstrip() for b in blocks))
    f.write('\n')

print(f"[cucumber] WRITE  {feature_path}  scenarios={len(re.findall(r'^  Scenario', ''.join(blocks), re.MULTILINE))}")
EOF "$STORY_FILE" "$FEATURE_FILE"

Generate Step Definition Stubs (TypeScript)

FEATURE_FILE="tests/features/user-auth/reset-password.feature"
STEPS_DIR="tests/step-definitions/user-auth"
mkdir -p "$STEPS_DIR"

python3 - <<'EOF'
import re, sys, os

feature_path = sys.argv[1]
steps_dir = sys.argv[2]
feature = open(feature_path).read()

# Collect unique step texts
steps = re.findall(r'^\s+(Given|When|Then|And|But) (.+)$', feature, re.MULTILINE)
seen = set()
unique = []
for keyword, text in steps:
    norm = re.sub(r'"[^"]+"', '"{string}"', text)
    norm = re.sub(r'\b\d+\b', '{int}', norm)
    if norm not in seen:
        seen.add(norm)
        unique.append((keyword if keyword not in ('And','But') else 'Given', norm, text))

# Build TypeScript file
feature_name = os.path.basename(feature_path).replace('.feature','')
out_path = os.path.join(steps_dir, f"{feature_name}.steps.ts")

if os.path.exists(out_path):
    print(f"[cucumber] SKIP  {out_path}  (already exists)")
    sys.exit(0)

lines = [
    "import { Given, When, Then } from '@cucumber/cucumber';",
    "",
]
for keyword, pattern, original in unique:
    fn = keyword.lower()
    # build parameter list from placeholders
    params = []
    if '{string}' in pattern:
        params += [f"arg{i}: string" for i in range(pattern.count('{string}'))]
    if '{int}' in pattern:
        params += [f"n{i}: number" for i in range(pattern.count('{int}'))]
    param_str = ', '.join(params)
    escaped = pattern.replace("'", "\\'")
    lines += [
        f"// {original}",
        f"{fn}('{escaped}', async ({param_str}) => {{",
        f"  // TODO: implement",
        f"  throw new Error('Pending: {escaped}');",
        f"}});",
        "",
    ]

with open(out_path, 'w') as f:
    f.write('\n'.join(lines))

print(f"[cucumber] STUBS  {out_path}  steps={len(unique)}")
EOF "$FEATURE_FILE" "$STEPS_DIR"

Generate Step Definition Stubs (Python / behave)

FEATURE_FILE="tests/features/user_auth/reset_password.feature"
STEPS_DIR="tests/features/user_auth"
mkdir -p "$STEPS_DIR"

python3 - <<'EOF'
import re, sys, os

feature_path = sys.argv[1]
steps_dir = sys.argv[2]
feature = open(feature_path).read()

steps = re.findall(r'^\s+(Given|When|Then|And|But) (.+)$', feature, re.MULTILINE)
seen = set()
unique = []
for keyword, text in steps:
    norm = re.sub(r'"[^"]+"', '"{text}"', text)
    norm = re.sub(r'\b\d+\b', '{n:d}', norm)
    if norm not in seen:
        seen.add(norm)
        unique.append((keyword if keyword not in ('And','But') else 'given', norm, text))

feature_name = os.path.basename(feature_path).replace('.feature','')
out_path = os.path.join(steps_dir, f"{feature_name}_steps.py")

if os.path.exists(out_path):
    print(f"[cucumber] SKIP  {out_path}  (already exists)")
    sys.exit(0)

lines = ["from behave import given, when, then", ""]
for keyword, pattern, original in unique:
    fn = keyword.lower()
    escaped = pattern.replace('"', '\\"')
    lines += [
        f"# {original}",
        f'@{fn}(u"{escaped}")',
        f"def step_impl(context):",
        f'    raise NotImplementedError(u"STEP: {fn} {escaped}")',
        "",
    ]

with open(out_path, 'w') as f:
    f.write('\n'.join(lines))

print(f"[cucumber] STUBS  {out_path}  steps={len(unique)}")
EOF "$FEATURE_FILE" "$STEPS_DIR"

Batch Sync (all stories → all feature files)

find docs/stories -name "*.md" ! -name "_template.md" | while read -r story; do
  EPIC=$(python3 -c "
import re
m = re.search(r'^epic:\s*[\"\'](.*?)[\"\']', open('$story').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
  SLUG=$(basename "$story" | sed 's/-[0-9]\{14\}-[0-9]\{4\}\.md$//')
  FEATURE_FILE="tests/features/$EPIC/$SLUG.feature"
  # extract-and-write step (see above) ...
  echo "[cucumber] SYNC  $story → $FEATURE_FILE"
done

Playwright + behave Integration

When the stack includes Playwright (requirements.txt contains playwright or behave), step definitions must follow these patterns. Read the QA agent's BDD section for the full antipatterns list.

environment.py template

import json
import threading
from playwright.sync_api import sync_playwright

BASE_URL = "http://localhost:3000"
DEFAULT_GEO = {"latitude": 40.7128, "longitude": -74.0060}

MOCK_FORECAST = {
    "daily": {
        "time": ["2025-01-01","2025-01-02","2025-01-03",
                 "2025-01-04","2025-01-05","2025-01-06","2025-01-07"],
        "temperature_2m_max": [22.1, 18.3, 15.0, 20.5, 25.2, 19.8, 17.4],
        "temperature_2m_min": [12.0, 10.5,  9.3, 13.2, 16.1, 11.4, 10.9],
        "weathercode":        [0,    3,     61,   0,    1,    80,   71 ],
        "precipitation_probability_max": [5, 30, 85, 10, 20, 70, 60],
        "windspeed_10m_max":  [12.0, 20.5, 35.2, 8.4, 15.6, 28.9, 22.3],
        "relative_humidity_2m_max": [55, 72, 90, 48, 62, 85, 78],
    }
}

def before_all(context):
    context.playwright = sync_playwright().start()
    context.browser = context.playwright.chromium.launch()

def after_all(context):
    context.browser.close()
    context.playwright.stop()

def before_scenario(context, scenario):
    context.browser_context = context.browser.new_context(
        permissions=["geolocation"],
        geolocation=DEFAULT_GEO,
    )
    context.page = context.browser_context.new_page()
    context._forecast_event = None

def after_scenario(context, scenario):
    context.page.close()
    context.browser_context.close()

# ── Helpers ────────────────────────────────────────────────────────────────────

def setup_forecast_route(page, error=False, delay_event=None):
    """Network-level Open-Meteo intercept. NEVER overrides window.fetch."""
    def handle(route):
        if delay_event is not None:
            delay_event.wait(timeout=30)
        if error:
            route.fulfill(status=500, content_type="application/json",
                          body='{"error":"service unavailable"}')
        else:
            route.fulfill(status=200, content_type="application/json",
                          body=json.dumps(MOCK_FORECAST))
    page.route("**/api.open-meteo.com/**", handle)

def navigate_and_wait(page):
    page.goto(BASE_URL, wait_until="domcontentloaded")
    page.wait_for_selector(".forecast-card", timeout=10_000)

Browser capability scenarios

# Geolocation granted — default, handled by before_scenario

# Geolocation denied — create context without the permission
context.browser_context = context.browser.new_context()  # no geolocation permission

# Geolocation unsupported — NO Playwright native API; add_init_script is acceptable HERE ONLY
context.page.add_init_script(
    "Object.defineProperty(navigator,'geolocation',{value:undefined,configurable:true});"
)

# Geolocation timeout — NO Playwright native API; add_init_script acceptable HERE ONLY
context.page.add_init_script(
    "navigator.geolocation.getCurrentPosition = function(){};"  # never calls back
)

Loading state (route with threading.Event)

# Setup step — route stays blocked until event is set
event = threading.Event()
setup_forecast_route(context.page, delay_event=event)
context._forecast_event = event

# Assert spinner visible while blocked
spinner = context.page.locator('[role="status"]')
spinner.wait_for(state="visible", timeout=3000)

# Unblock route → cards should appear
context._forecast_event.set()
context.page.wait_for_selector(".forecast-card", timeout=5000)

API error state

setup_forecast_route(context.page, error=True)
# route returns HTTP 500 — app must handle gracefully

Antipatterns checklist (rubber duck will flag these)

Antipattern Why it's wrong
add_init_script("window.fetch = ...") Bypasses all app network code; test passes regardless of implementation
page.evaluate("window._appResolve()") Reaches into app internals; not observable behaviour
add_init_script(GEO_SUCCESS_SCRIPT) for geolocation Playwright's context API exists — use it
page.evaluate("window.__mock = ...") Same as window.fetch override — leaks test state into app
Assertions checking internal state (data-* set by test, not app) Test the rendered output, not the test's own injected data

Failure Modes

Condition Action
Story file has no gherkin fenced block Log NO_GHERKIN. Skip. Do not create empty feature file.
Feature file already exists with manual edits Compare checksums. If changed: log MANUAL_EDIT — skip overwrite. Never overwrite manual edits.
Step file already exists Skip generation. Log SKIP. Agents implement stubs; generator does not regenerate.
Stack is unknown Log warning. Write .feature file only; skip step stubs.
Gherkin parse error (malformed block) Log the offending line. Skip the file. Do not attempt partial extraction.

Logging

[cucumber] WRITE   tests/features/user-auth/reset-password.feature  scenarios=3
[cucumber] STUBS   tests/step-definitions/user-auth/reset-password.steps.ts  steps=7
[cucumber] SKIP    tests/step-definitions/user-auth/login.steps.ts  (already exists)
[cucumber] NO_GHERKIN  docs/stories/auth-spike-20250416143000-0002.md  (spike — expected)
用于读取和写入W3C DTCG格式的设计令牌(tokens.json),并自动生成CSS自定义属性、Tailwind配置及Mantine主题对象。tokens.json为唯一权威来源,其他输出均由其派生生成,严禁手动编辑。
修改或生成设计令牌 需要更新CSS变量、Tailwind配置或Mantine主题时
.cursor/skills/design-tokens/SKILL.md
npx skills add kinncj/Heimdall --skill design-tokens -g -y
SKILL.md
Frontmatter
{
    "name": "design-tokens",
    "description": "Read and write design tokens in W3C DTCG format (tokens.json) and emit CSS, Tailwind, and Mantine outputs. Use when modifying or generating design tokens."
}

SKILL: design-tokens

Purpose

Read and write design tokens in W3C Design Token Community Group (DTCG) format (tokens.json). Emit framework-specific outputs: CSS custom properties, Tailwind config, and Mantine theme object. The tokens.json in docs/design/identity/ is the canonical source of truth; all framework outputs are derived and regenerated — never manually edited.

Token File: docs/design/identity/tokens.json

W3C DTCG format. Each token is an object with $value and $type:

{
  "$schema": "https://design-tokens.org/schema/v1.0.0",
  "_meta": { "status": "draft", "version": "1.0.0" },

  "color": {
    "brand": {
      "primary":   { "$value": "#2563eb", "$type": "color" },
      "secondary": { "$value": "#7c3aed", "$type": "color" },
      "accent":    { "$value": "#0ea5e9", "$type": "color" }
    },
    "semantic": {
      "success": { "$value": "#16a34a", "$type": "color" },
      "warning": { "$value": "#d97706", "$type": "color" },
      "error":   { "$value": "#dc2626", "$type": "color" },
      "info":    { "$value": "#0284c7", "$type": "color" }
    },
    "neutral": {
      "50":  { "$value": "#f8fafc", "$type": "color" },
      "500": { "$value": "#64748b", "$type": "color" },
      "900": { "$value": "#0f172a", "$type": "color" }
    },
    "surface": {
      "background": { "$value": "#ffffff", "$type": "color" },
      "foreground": { "$value": "#0f172a", "$type": "color" },
      "muted":      { "$value": "#f1f5f9", "$type": "color" },
      "border":     { "$value": "#e2e8f0", "$type": "color" }
    }
  },

  "typography": {
    "fontFamily": {
      "sans": { "$value": "Inter, system-ui, sans-serif", "$type": "fontFamily" },
      "mono": { "$value": "JetBrains Mono, monospace",   "$type": "fontFamily" }
    },
    "fontSize": {
      "sm":   { "$value": "0.875rem", "$type": "dimension" },
      "base": { "$value": "1rem",     "$type": "dimension" },
      "lg":   { "$value": "1.125rem", "$type": "dimension" },
      "xl":   { "$value": "1.25rem",  "$type": "dimension" },
      "2xl":  { "$value": "1.5rem",   "$type": "dimension" },
      "4xl":  { "$value": "2.25rem",  "$type": "dimension" }
    }
  },

  "spacing": {
    "1": { "$value": "0.25rem", "$type": "dimension" },
    "2": { "$value": "0.5rem",  "$type": "dimension" },
    "4": { "$value": "1rem",    "$type": "dimension" },
    "8": { "$value": "2rem",    "$type": "dimension" }
  },

  "radius": {
    "sm":   { "$value": "0.125rem", "$type": "dimension" },
    "md":   { "$value": "0.375rem", "$type": "dimension" },
    "lg":   { "$value": "0.5rem",   "$type": "dimension" },
    "full": { "$value": "9999px",   "$type": "dimension" }
  },

  "shadow": {
    "sm": { "$value": "0 1px 2px 0 rgb(0 0 0 / 0.05)", "$type": "shadow" },
    "md": { "$value": "0 4px 6px -1px rgb(0 0 0 / 0.1)", "$type": "shadow" },
    "lg": { "$value": "0 10px 15px -3px rgb(0 0 0 / 0.1)", "$type": "shadow" }
  }
}

Emit: CSS Custom Properties

Output to docs/design/identity/tokens.css:

import json, re

def flatten(obj, prefix=""):
    result = {}
    for k, v in obj.items():
        if k.startswith("$") or k.startswith("_"):
            continue
        key = f"{prefix}-{k}" if prefix else k
        if isinstance(v, dict) and "$value" in v:
            result[key] = v["$value"]
        elif isinstance(v, dict):
            result.update(flatten(v, key))
    return result

tokens = json.load(open("docs/design/identity/tokens.json"))
flat = flatten(tokens)

lines = [":root {"]
for name, value in sorted(flat.items()):
    css_var = "--" + name.replace(".", "-")
    lines.append(f"  {css_var}: {value};")
lines.append("}")

with open("docs/design/identity/tokens.css", "w") as f:
    f.write("\n".join(lines) + "\n")

print(f"[design-tokens] CSS  docs/design/identity/tokens.css  vars={len(flat)}")

Emit: Tailwind Config

Output to docs/design/identity/tailwind.tokens.js:

import json

tokens = json.load(open("docs/design/identity/tokens.json"))

def extract_colors():
    result = {}
    for group, values in tokens.get("color", {}).items():
        result[group] = {}
        for name, token in values.items():
            if "$value" in token:
                result[group][name] = token["$value"]
    return result

colors = extract_colors()
font_family = {
    k: v["$value"]
    for k, v in tokens.get("typography", {}).get("fontFamily", {}).items()
}

config = f"""/** @type {{import('tailwindcss').Config}} */
// AUTO-GENERATED — edit docs/design/identity/tokens.json, not this file
module.exports = {{
  theme: {{
    extend: {{
      colors: {json.dumps(colors, indent(6))},
      fontFamily: {json.dumps(font_family, indent(6))},
    }},
  }},
}};
"""

with open("docs/design/identity/tailwind.tokens.js", "w") as f:
    f.write(config)

print("[design-tokens] TAILWIND  docs/design/identity/tailwind.tokens.js")

Emit: Mantine Theme

Output to docs/design/identity/mantine.theme.ts:

import json

tokens = json.load(open("docs/design/identity/tokens.json"))

primary = tokens["color"]["brand"]["primary"]["$value"]
secondary = tokens["color"]["brand"]["secondary"]["$value"]

theme = f"""// AUTO-GENERATED — edit docs/design/identity/tokens.json, not this file
import {{ createTheme }} from '@mantine/core';

export const theme = createTheme({{
  primaryColor: 'brand',
  colors: {{
    brand: [
      '{tokens["color"]["neutral"]["50"]["$value"]}',
      '{tokens["color"]["neutral"]["100"]["$value"] if "100" in tokens["color"]["neutral"] else "#f1f5f9"}',
      '{tokens["color"]["neutral"]["200"]["$value"] if "200" in tokens["color"]["neutral"] else "#e2e8f0"}',
      '{tokens["color"]["neutral"]["300"]["$value"] if "300" in tokens["color"]["neutral"] else "#cbd5e1"}',
      '{tokens["color"]["neutral"]["400"]["$value"] if "400" in tokens["color"]["neutral"] else "#94a3b8"}',
      '{tokens["color"]["neutral"]["500"]["$value"]}',
      '{primary}',
      '{tokens["color"]["neutral"]["700"]["$value"] if "700" in tokens["color"]["neutral"] else "#334155"}',
      '{tokens["color"]["neutral"]["800"]["$value"] if "800" in tokens["color"]["neutral"] else "#1e293b"}',
      '{tokens["color"]["neutral"]["900"]["$value"]}',
    ],
  }},
  fontFamily: '{tokens["typography"]["fontFamily"]["sans"]["$value"]}',
  fontFamilyMonospace: '{tokens["typography"]["fontFamily"]["mono"]["$value"]}',
}});
"""

with open("docs/design/identity/mantine.theme.ts", "w") as f:
    f.write(theme)

print("[design-tokens] MANTINE  docs/design/identity/mantine.theme.ts")

Emit: Terminal Theme (tui target)

When design.target is tui, emit docs/design/identity/terminal-theme.json from tokens.json instead of the CSS/Tailwind/Mantine outputs. Map each color role to an ANSI-256 index (and the hex) plus a lipgloss style name, and record the foreground/background pairs with their contrast ratios so the a11y auditor can flag any pair under WCAG 2.2 AA. Shape:

{
  "roles": {
    "primary":    { "fg": "#7aa2f7", "ansi256": 111, "lipgloss": "primary" },
    "muted":      { "fg": "#565f89", "ansi256": 60,  "lipgloss": "muted" },
    "accent":     { "fg": "#bb9af7", "ansi256": 141, "lipgloss": "accent" },
    "error":      { "fg": "#f7768e", "ansi256": 204, "lipgloss": "error" },
    "success":    { "fg": "#9ece6a", "ansi256": 149, "lipgloss": "success" },
    "background": { "bg": "#1a1b26", "ansi256": 234 },
    "foreground": { "fg": "#c0caf5", "ansi256": 189 }
  },
  "pairs": [
    { "role": "foreground", "on": "background", "contrast": 12.6 },
    { "role": "muted",      "on": "background", "contrast": 3.4 }
  ]
}

Run All Emitters

python3 - <<'EOF'
# Run all three emitters in sequence
# (inline the three scripts above, or import from scripts/emit-tokens.py)
EOF

Update Tokens (read-write)

To update a single token value:

import json, sys

tokens = json.load(open("docs/design/identity/tokens.json"))
# Set tokens["color"]["brand"]["primary"]["$value"] = "#1d4ed8"
# Write back, then re-run all emitters
json.dump(tokens, open("docs/design/identity/tokens.json", "w"), indent=2)

Always re-emit all framework outputs after any token change.

Failure Modes

Condition Action
tokens.json malformed Log parse error with line number. Do not emit partial output.
Missing required token key Log MISSING_TOKEN: {path}. Emit with placeholder TODO value.
Emitted file differs from committed Acceptable — these are always regenerated.
_meta.status != approved Log warning. Emit anyway (dev workflow). Gate is in visual-identity approval, not here.

Logging

[design-tokens] READ    docs/design/identity/tokens.json  tokens=47
[design-tokens] CSS     docs/design/identity/tokens.css   vars=47
[design-tokens] TAILWIND docs/design/identity/tailwind.tokens.js
[design-tokens] MANTINE  docs/design/identity/mantine.theme.ts
[design-tokens] MISSING_TOKEN  color.neutral.100  — using placeholder
提供Docker和Docker Compose最佳实践,涵盖多阶段构建、健康检查配置及.dockerignore规则。指导编写安全高效的容器化应用,强调非root运行、缓存优化及镜像扫描验证。
编写或审查Dockerfile时 创建或修改docker-compose配置文件时
.cursor/skills/docker-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill docker-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "docker-patterns",
    "description": "Apply Docker and Docker Compose best practices for containerising services. Use when writing or reviewing Dockerfiles and compose configs."
}

SKILL: Docker Patterns

Multi-Stage Dockerfile Pattern

# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app

FROM base AS deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

FROM base AS dev-deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

FROM base AS build
COPY --from=dev-deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs22-debian12 AS production
WORKDIR /app
USER 1001
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["dist/main.js"]

docker-compose Health Check Pattern

services:
  app:
    build:
      context: .
      target: production
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 5s
      retries: 10

.dockerignore

node_modules
.git
.github
dist
build
coverage
.next
*.log
.env*
!.env.example
README.md
docs
tests

Key Rules

  • ALWAYS use multi-stage builds for production images.
  • ALWAYS run as non-root user (USER 1001 or named user).
  • ALWAYS add health checks to all services.
  • NEVER copy .env files into images.
  • Use --mount=type=cache for package manager caches.
  • Use distroless or alpine base images.
  • Use docker compose up -d --wait to wait for health checks.

Verification

# Build test
docker build --target production -t test-image .

# Security scan
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image test-image

# Start and verify health
docker compose up -d --wait
docker compose ps
识别并标注架构决策的云端成本影响。用于审查基础设施变更或设计新服务,涵盖计算、存储、网络及第三方API的成本驱动因素,提供Terraform注释格式和月度估算模板,辅助进行FinOps成本优化评估。
审查基础设施变更 设计新服务 评估架构决策的成本影响
.cursor/skills/finops-review/SKILL.md
npx skills add kinncj/Heimdall --skill finops-review -g -y
SKILL.md
Frontmatter
{
    "name": "finops-review",
    "description": "Identify and annotate cloud cost implications of architectural decisions. Use when reviewing infrastructure changes or designing new services."
}

SKILL: FinOps Review

Purpose

Identify and annotate the cloud cost implications of architectural decisions.

Cost Driver Categories

Compute

  • Lambda/Functions: invocation count x duration x memory
  • EC2/VMs: instance type x hours + data transfer
  • Containers: CPU/memory reservation x hours
  • Edge functions: request count x execution time

Storage

  • Object storage (S3/GCS/Blob): GB stored + requests + egress
  • Database: instance size + storage + IOPS + backups
  • Cache: node size x hours

Networking

  • Egress: GB transferred out (most expensive)
  • CDN: requests + GB served
  • Load balancer: hours + LCU

Third-party APIs

  • Stripe: 2.9% + $0.30 per transaction
  • Supabase: row reads/writes, storage, Edge Function invocations
  • Vercel: bandwidth, function invocations, seats

Annotation Format (Terraform)

resource "aws_db_instance" "main" {
  # finops: ~$180/mo (db.t3.medium, 100GB gp3, 1 AZ)
  # finops: scale-trigger: >70% CPU or >80% storage
  instance_class = "db.t3.medium"
  ...
}

Architecture Review Questions

  1. What scales with user count? What scales with data volume?
  2. Are there N+1 API calls that will cost $$ at scale?
  3. Is egress minimized? (Cache aggressively, colocate services)
  4. Are reserved instances/savings plans applicable?
  5. What is the cost at 10x current load?

Monthly Estimate Template

## Cost Estimate: {Feature}

| Service | Config | Est. Monthly |
|---------|--------|-------------|
| RDS PostgreSQL | db.t3.medium, 100GB | $180 |
| ElastiCache Redis | cache.t3.micro | $25 |
| ECS Fargate | 2 tasks, 0.5 vCPU, 1GB | $30 |
| ALB | 1 ALB, ~1M requests | $20 |
| **Total** | | **~$255/mo** |

Scales linearly with: [traffic volume / data volume]
自动化管理 GitHub Issue 生命周期,涵盖创建、查看、编辑标签与指派、评论及关闭。支持通过 CLI 操作 Issue 以关联故事制品,实现从需求到验收的全流程无人值守管理。
需要为故事创建新的 GitHub Issue 更新 Issue 状态、标签或指派人员 在 Issue 中记录 TDD 测试进度或阻塞信息 查询特定阶段的 Issue 列表
.cursor/skills/gh-issues/SKILL.md
npx skills add kinncj/Heimdall --skill gh-issues -g -y
SKILL.md
Frontmatter
{
    "name": "gh-issues",
    "description": "Create, read, update, and link GitHub Issues as story artifacts throughout the story lifecycle. Use when managing issues for stories."
}

SKILL: gh-issues

Purpose

Create, read, update, and link GitHub Issues as first-class story artifacts. Agents use this skill to manage issue lifecycle — from PO story creation through QA closure — without human intervention.

Inputs

Field Source Example
title story frontmatter "User can reset password"
body_file story file path docs/stories/auth-reset-0001.md
labels story frontmatter + phase "type:feature,priority:high,phase:discover"
milestone project.config.yaml "v1.0"
assignee orchestrator context "@me"
issue_number previously created issue 42

Outputs

Field Description
issue_number integer, use for all subsequent operations
issue_url full GitHub URL for logging
issue_node_id GraphQL node ID, needed for Projects v2 card add

Create an Issue

# Capture issue number and URL
RESULT=$(gh issue create \
  --title "{title}" \
  --body-file "{body_file}" \
  --label "{labels}" \
  --milestone "{milestone}" \
  --json number,url,id)

ISSUE_NUMBER=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['number'])")
ISSUE_URL=$(echo "$RESULT"    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['url'])")
ISSUE_NODE=$(echo "$RESULT"   | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])")

View an Issue

gh issue view {issue_number} --json number,title,state,labels,body

Edit Labels and Assignee

# Add label, remove old phase label
gh issue edit {issue_number} \
  --add-label "phase:architect" \
  --remove-label "phase:discover"

# Assign to current actor
gh issue edit {issue_number} --add-assignee "@me"

Add a Comment

gh issue comment {issue_number} \
  --body "{message}"

Comment conventions by role:

Event Template
Phase start "Phase {N} {PHASE_NAME}: starting. Artifact target: {path}"
Phase complete "Phase {N} {PHASE_NAME}: complete. Artifacts: {path}"
TDD red "RED: Failing test at {test_path} — {failure_message}"
TDD green "GREEN: {test_count} tests passing."
Blocked "BLOCKED: {agent} failed 3x on {task}. Human intervention required."

Close an Issue

# Close with final validation summary
gh issue close {issue_number} \
  --comment "All acceptance criteria passing. Validation: {summary}"

List Issues

# All open issues for current phase
gh issue list --label "phase:implement" --state open --json number,title,labels

# Blocked issues
gh issue list --label "blocked" --state open

Link Issues (parent / blocks)

GitHub Issues has no native parent field — use body references and labels:

# In the child issue body, add:
# "Closes #{parent_number}" → auto-closes parent when child closes
# "Part of #{epic_issue_number}"

# For blocking relationships, comment on the blocked issue:
gh issue comment {blocked_number} \
  --body "Blocked by #{blocker_number} — {reason}"
gh issue edit {blocked_number} --add-label "blocked"

Failure Modes

Condition Action
gh not authenticated Stop. Output: gh auth login required. Do not retry.
Issue already exists with same title Check with gh issue list --search "{title}" --state all. Skip create if found; return existing number.
Label does not exist Create label first via gh label create. See gh-labels-milestones skill.
Milestone not found Create milestone first: gh api repos/{owner}/{repo}/milestones -f title="{name}".
Rate limit hit Wait 60 seconds, retry once. If second attempt fails, stop and log.

Logging

Always log after every gh issue mutation:

[gh-issues] CREATE  #42  "User can reset password"  labels=[type:feature,priority:high]
[gh-issues] COMMENT #42  phase:implement start
[gh-issues] CLOSE   #42  all criteria passing

Format: [gh-issues] {ACTION} #{number} {description}

幂等式初始化与同步 GitHub 仓库的标签和里程碑。通过脚本自动创建缺失项或更新颜色描述,确保项目阶段、类型、优先级及规范状态标签的一致性,适用于新项目启动或 CI 流程。
新建 GitHub 仓库时初始化标签体系 CI/CD 流水线中同步标签定义 修复或统一现有仓库的标签配置
.cursor/skills/gh-labels-milestones/SKILL.md
npx skills add kinncj/Heimdall --skill gh-labels-milestones -g -y
SKILL.md
Frontmatter
{
    "name": "gh-labels-milestones",
    "description": "Bootstrap and sync GitHub labels and milestones idempotently. Use when setting up a new repository or syncing label definitions."
}

SKILL: gh-labels-milestones

Purpose

Bootstrap and sync GitHub labels and milestones idempotently. Labels and milestones are created if absent; existing ones are updated if color or description differs. Never delete — only add or update.

Label Bootstrap

Use this at project start (maple labels) and in CI to guarantee label state.

# Idempotent label create-or-update
upsert_label() {
  local name="$1" color="$2" description="$3"
  if gh label list --json name --jq '.[].name' | grep -qx "$name"; then
    gh label edit "$name" --color "$color" --description "$description"
  else
    gh label create "$name" --color "$color" --description "$description"
  fi
}

Label Groups

Phase Labels (pipeline position)

upsert_label "phase:discover"   "0075ca" "Phase 1: Discovery"
upsert_label "phase:architect"  "0075ca" "Phase 2: Architecture"
upsert_label "phase:plan"       "0075ca" "Phase 3: Planning"
upsert_label "phase:infra"      "0075ca" "Phase 4: Infrastructure"
upsert_label "phase:implement"  "0075ca" "Phase 5: Implementation"
upsert_label "phase:validate"   "0075ca" "Phase 6: Validation"
upsert_label "phase:document"   "0075ca" "Phase 7: Documentation"
upsert_label "phase:done"       "0075ca" "Phase 8: Complete"

Type Labels (work classification)

upsert_label "type:feature"     "0052cc" "New capability"
upsert_label "type:bug"         "d73a4a" "Defect"
upsert_label "type:spike"       "e4e669" "Research / time-boxed exploration"
upsert_label "type:chore"       "ededed" "Non-functional maintenance"
upsert_label "type:refactor"    "ededed" "Code restructuring, no behavior change"
upsert_label "type:docs"        "0075ca" "Documentation only"

Priority Labels (MoSCoW)

upsert_label "priority:critical" "b60205" "Must ship — blocks launch"
upsert_label "priority:high"     "e11d48" "Must have"
upsert_label "priority:medium"   "f97316" "Should have"
upsert_label "priority:low"      "84cc16" "Could have"
upsert_label "priority:wontfix"  "ffffff" "Won't have this cycle"

Spec / Story Kit Labels

upsert_label "spec:problem"     "7057ff" "Problem statement written"
upsert_label "spec:approved"    "7057ff" "Spec approved by PO"
upsert_label "spec:in-review"   "7057ff" "Spec under review"

Design Labels

upsert_label "design:pending"      "fbca04" "Awaiting design"
upsert_label "design:in-progress"  "fbca04" "Design work active"
upsert_label "design:approved"     "fbca04" "Design approved"
upsert_label "design:a11y-passed"  "fbca04" "Accessibility review passed"

ADR Labels

upsert_label "adr:required"    "5319e7" "ADR must be written before implementation"
upsert_label "adr:in-progress" "5319e7" "ADR being authored"
upsert_label "adr:complete"    "5319e7" "ADR accepted"
upsert_label "adr:rejected"    "5319e7" "ADR rejected — see comments"

UI / Accessibility Labels

upsert_label "ui:required"     "fef2c0" "Has UI surface — design review required"
upsert_label "ui:in-progress"  "fef2c0" "UI implementation active"
upsert_label "ui:complete"     "fef2c0" "UI complete and reviewed"

Status Labels

upsert_label "in-progress" "0052cc" "Work started"
upsert_label "blocked"     "b60205" "Blocked — needs human"
upsert_label "validated"   "0e8a16" "All tests pass"
upsert_label "tdd:red"     "d73a4a" "Failing test written"
upsert_label "tdd:green"   "0e8a16" "Tests passing"

Milestone Bootstrap

# Idempotent milestone create
upsert_milestone() {
  local title="$1" due="$2" description="$3"
  EXISTING=$(gh api "repos/{owner}/{repo}/milestones" \
    --jq ".[] | select(.title == \"$title\") | .number" 2>/dev/null)
  if [ -n "$EXISTING" ]; then
    gh api "repos/{owner}/{repo}/milestones/$EXISTING" \
      -X PATCH \
      -f title="$title" \
      -f due_on="${due}T00:00:00Z" \
      -f description="$description"
  else
    gh api "repos/{owner}/{repo}/milestones" \
      -X POST \
      -f title="$title" \
      -f due_on="${due}T00:00:00Z" \
      -f description="$description"
  fi
}

# Example usage
upsert_milestone "v1.0" "2025-12-31" "Initial release"

Assign Labels to an Issue

# Add labels (idempotent — gh ignores if already present)
gh issue edit {issue_number} \
  --add-label "type:feature,priority:high,phase:discover"

# Remove a specific label
gh issue edit {issue_number} --remove-label "phase:discover"

Failure Modes

Condition Action
Label name collision (wrong color) gh label edit — update in place
Milestone already exists Patch via API — do not create duplicate
gh not authenticated Stop immediately. Do not retry.
Label with special characters URL-encode the label name in API calls

Logging

[gh-labels] UPSERT  label="type:feature"    color=0052cc
[gh-labels] SKIP    label="priority:high"   (unchanged)
[gh-milestones] CREATE  "v1.0"  due=2025-12-31
[gh-milestones] SKIP    "v1.0"  already exists
管理GitHub Projects v2看板,支持添加Issue卡片、更新字段值及查询状态。优先使用gh CLI,兼容GraphQL API,需读取项目配置并获取字段与选项ID以完成操作。
需要向GitHub项目看板添加Issue时 需要更新项目看板的字段状态或类型时 需要查询或管理GitHub Projects v2看板状态时
.cursor/skills/gh-projects/SKILL.md
npx skills add kinncj/Heimdall --skill gh-projects -g -y
SKILL.md
Frontmatter
{
    "name": "gh-projects",
    "description": "Manage GitHub Projects v2 board: add issues, update field values, and query board state. Use when managing project boards."
}

SKILL: gh-projects

Purpose

Manage GitHub Projects v2 board operations: add issues as cards, update field values (status, type, ADR required), and query board state. Uses gh project CLI where available; falls back to gh api graphql for operations the CLI does not expose.

Inputs

Field Source Example
project_number project.config.yaml 3
project_node_id project.config.yaml "PVT_kwDO..."
issue_node_id gh-issues skill output "I_kwDO..."
issue_number gh-issues skill output 42
field_name project field name "Status"
field_value target option name "In progress"

Read project config

PROJECT_NUMBER=$(python3 -c "
import sys
for line in open('project.config.yaml'):
    if 'project_number:' in line:
        print(line.split(':')[1].strip())
        break
")

PROJECT_NODE=$(python3 -c "
import sys
for line in open('project.config.yaml'):
    if 'project_node_id:' in line:
        print(line.split(':', 1)[1].strip())
        break
")

Add an Issue to the Project Board

# CLI (preferred)
gh project item-add "$PROJECT_NUMBER" \
  --owner "{owner}" \
  --url "https://github.com/{owner}/{repo}/issues/{issue_number}"

# GraphQL fallback (when CLI unavailable or for automation)
gh api graphql -f query='
  mutation($project: ID!, $content: ID!) {
    addProjectV2ItemById(input: {projectId: $project, contentId: $content}) {
      item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f content="{issue_node_id}" \
  --jq '.data.addProjectV2ItemById.item.id'

Save the returned item_id — required for field updates.

Get Field and Option IDs

Field updates require the field's node ID and the option's node ID (for single-select fields).

FIELDS=$(gh api graphql -f query='
  query($project: ID!) {
    node(id: $project) {
      ... on ProjectV2 {
        fields(first: 20) {
          nodes {
            ... on ProjectV2Field { id name }
            ... on ProjectV2SingleSelectField {
              id name
              options { id name }
            }
          }
        }
      }
    }
  }' \
  -f project="$PROJECT_NODE")

# Extract field ID by name
FIELD_ID=$(echo "$FIELDS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for f in d['data']['node']['fields']['nodes']:
    if f.get('name') == '{field_name}':
        print(f['id'])
        break
")

# Extract option ID by value (single-select fields)
OPTION_ID=$(echo "$FIELDS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for f in d['data']['node']['fields']['nodes']:
    if f.get('name') == '{field_name}':
        for opt in f.get('options', []):
            if opt['name'] == '{field_value}':
                print(opt['id'])
                break
")

Update a Single-Select Field (Status, Type, ADR Required)

gh api graphql -f query='
  mutation($project: ID!, $item: ID!, $field: ID!, $option: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $project
      itemId: $item
      fieldId: $field
      value: { singleSelectOptionId: $option }
    }) {
      projectV2Item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f item="{item_id}" \
  -f field="$FIELD_ID" \
  -f option="$OPTION_ID"

Update a Text Field (Epic, Specialist)

gh api graphql -f query='
  mutation($project: ID!, $item: ID!, $field: ID!, $value: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $project
      itemId: $item
      fieldId: $field
      value: { text: $value }
    }) {
      projectV2Item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f item="{item_id}" \
  -f field="$FIELD_ID" \
  -f value="{text_value}"

Standard Field Updates by Pipeline Phase

Phase Field Value
Discover Status Todo
Architect Status In progress
Implement Status In progress
Validate Status In Review
Done Status Done

Query Board State

# List all items with status
gh project item-list "$PROJECT_NUMBER" \
  --owner "{owner}" \
  --format json \
  --jq '.items[] | {id, title: .content.title, status: .fieldValues[] | select(.field.name=="Status") | .name}'

Failure Modes

Condition Action
project_node_id missing from config Run maple project to bootstrap. Stop until resolved.
Item already on board Query before adding. gh project item-list and check content URL. Skip if present.
Field ID not found Re-fetch fields. Field names are case-sensitive.
Option ID not found List available options from field metadata. Do not guess.
GraphQL rate limit Wait 60 seconds. Retry once. Log and stop on second failure.

Logging

[gh-projects] ADD    #42  → project #{project_number}  item_id={id}
[gh-projects] UPDATE #42  field=Status  value="In progress"
[gh-projects] SKIP   #42  already on board
用于生成符合规范的Gherkin故事文件,自动处理ID分配、时间戳生成及文件名构建。提供标准模板与验证规则,确保行为驱动开发文档的一致性与有效性。
编写新的用户故事或功能需求 更新现有的Gherkin验收标准文件 需要按照特定命名规范创建故事文档
.cursor/skills/gherkin-authoring/SKILL.md
npx skills add kinncj/Heimdall --skill gherkin-authoring -g -y
SKILL.md
Frontmatter
{
    "name": "gherkin-authoring",
    "description": "Produce valid, consistently named Gherkin story files with correct IDs, timestamps, and tags. Use when writing or updating story files."
}

SKILL: gherkin-authoring

Purpose

Produce valid, consistently named Gherkin story files. Handles ID allocation, timestamp generation, filename construction, tag formatting, and scenario validation. Used by the product-owner agent whenever a new story is written or updated.

Story File Naming Convention

<epic-slug>-<story-slug>-<timestamp>-<NNNN>.md
Part Format Example
epic-slug kebab-case, 2–4 words user-auth
story-slug kebab-case, 2–5 words reset-password
timestamp YYYYMMDDHHMMSS 20250416143000
NNNN 4-digit zero-padded, sequential within epic 0003

Full example: user-auth-reset-password-20250416143000-0003.md

Allocate the Next Story ID

# Count existing stories for an epic to get next sequential ID
EPIC="user-auth"
NEXT_ID=$(ls docs/stories/${EPIC}-* 2>/dev/null \
  | grep -oE '\-[0-9]{4}\.md$' \
  | grep -oE '[0-9]{4}' \
  | sort -n | tail -1 \
  | python3 -c "import sys; n=sys.stdin.read().strip(); print(f'{int(n)+1:04d}' if n else '0001')")
[ -z "$NEXT_ID" ] && NEXT_ID="0001"
echo "$NEXT_ID"

Generate Filename

EPIC="user-auth"
STORY="reset-password"
TS=$(date -u +"%Y%m%d%H%M%S")
ID="$NEXT_ID"   # from allocation step above
FILENAME="${EPIC}-${STORY}-${TS}-${ID}.md"
echo "$FILENAME"   # user-auth-reset-password-20250416143000-0001.md

Story File Template

Every story file must contain:

---
id: "{EPIC}-{NNNN}"
title: "{Human-readable story title}"
epic: "{epic-slug}"
priority: "high|medium|low|critical"
ui: false
adr_required: false
milestone: "{milestone name}"
labels:
  - "type:feature"
  - "priority:high"
  - "phase:discover"
issue_number: null
issue_url: null
---

## Story

**As a** {actor},
**I want** {capability},
**so that** {business outcome}.

## Acceptance Criteria

```gherkin
@story:{EPIC}-{NNNN} @epic:{epic-slug} @priority:{level}
Feature: {Feature title}

  Scenario: {scenario title}
    Given {precondition}
    When {action}
    Then {expected outcome}

Definition of Done

  • All Gherkin scenarios have passing step implementations
  • Unit tests written and passing
  • Integration tests written and passing (if applicable)
  • Code reviewed and approved
  • No regressions in related features
  • Documentation updated (if applicable)

ADR Links


## Gherkin Validation Rules

Before writing a scenario, verify:

1. **Single responsibility**: each `Scenario` tests exactly one behavior.
2. **Given-When-Then order**: never skip or reorder.
3. **No implementation detail in steps**: steps describe intent, not code.
4. **Declarative steps**: `Given the user is logged in` ✓ — `Given I call POST /auth/login` ✗
5. **Tags present**: `@story:`, `@epic:`, `@priority:` required on every Feature block.
6. **No `And` as first step**: `And` is only valid after a `Given`, `When`, or `Then`.

## Background Block (shared preconditions)

```gherkin
Feature: Password Reset

  Background:
    Given the user has a verified account
    And the account is not locked

  Scenario: Successful reset request
    When the user submits their email address
    Then a reset link is sent to that address

Use Background when 2+ scenarios share the same preconditions.

Scenario Outline (data-driven)

  Scenario Outline: Reject invalid email formats
    When the user submits "<email>"
    Then they see a validation error "<message>"

    Examples:
      | email        | message                  |
      | notanemail   | Invalid email format     |
      | @nodomain    | Invalid email format     |
      | a@b          | Domain must have TLD     |

Tags Reference

Tag Required Description
@story:{id} Yes Links scenario to story file
@epic:{slug} Yes Groups stories by epic
@priority:{level} Yes critical, high, medium, low
@wip No Work in progress — excluded from CI by default
@ui No Requires browser/UI driver
@integration No Requires external services
@smoke No Runs in production smoke suite

Failure Modes

Condition Action
Story ID collision (same NNNN exists) Re-run allocation step. Never overwrite an existing file.
docs/stories/ does not exist Create it: mkdir -p docs/stories/
Scenario has no When Invalid — split into two scenarios or add action step
Missing required tags Add tags before writing the file
Title exceeds 72 characters Shorten. Titles appear in issue headings and PR titles.

Logging

[gherkin] ALLOCATE  epic=user-auth  next_id=0003
[gherkin] CREATE    docs/stories/user-auth-reset-password-20250416143000-0003.md
[gherkin] VALIDATE  scenarios=3  tags=ok  structure=ok
提供GitHub CLI (gh) 全面参考,涵盖Issue创建/查询/更新、PR草稿合并及Actions搜索。通过标准化标签与生命周期注释管理任务流,适用于所有GitHub交互场景。
需要创建或管理GitHub Issue时 需要提交、审查或合并Pull Request时 查询仓库状态、分支或CI结果时 使用GitHub Actions或Project Boards时
.cursor/skills/github-cli/SKILL.md
npx skills add kinncj/Heimdall --skill github-cli -g -y
SKILL.md
Frontmatter
{
    "name": "github-cli",
    "description": "Comprehensive gh CLI reference — issues, tasks, PRs, repos, branches, Actions, search, and project board. Use whenever interacting with GitHub."
}

SKILL: GitHub CLI

Repo Context (always run first)

# Who am I, what repo am I in
gh auth status
gh repo view --json name,owner,defaultBranchRef,url,isPrivate

# Open issues + PRs at a glance
gh issue list --state open --limit 20 --json number,title,labels,assignees
gh pr list --state open --json number,title,headRefName,statusCheckRollup

Issues as Tasks

Create

# Feature story
gh issue create \
  --title "feat: {title}" \
  --body-file docs/stories/{slug}/Story.md \
  --label "type:feature,priority:high,phase:discover" \
  --milestone "v1.0" \
  --assignee "@me"

# Bug
gh issue create \
  --title "fix: {title}" \
  --body "## Steps to reproduce\n{steps}\n\n## Expected\n{expected}\n\n## Actual\n{actual}" \
  --label "type:bug,priority:high"

# Task (sub-work, no story file)
gh issue create \
  --title "task: {title}" \
  --body "{description}" \
  --label "type:task" \
  --assignee "@me"

Read

gh issue view {number}
gh issue view {number} --json number,title,state,body,labels,assignees,comments,milestone

# Search
gh issue list --search "label:blocked" --state open
gh issue list --search "assignee:@me is:open"
gh issue list --search "phase:implement" --label "tdd:red"
gh issue list --state open --label "type:feature" --milestone "v1.0"

Update

# Phase transition
gh issue edit {number} \
  --add-label "phase:architect" \
  --remove-label "phase:discover"

# Assign / unassign
gh issue edit {number} --add-assignee "@me"
gh issue edit {number} --remove-assignee "{login}"

# Change milestone
gh issue edit {number} --milestone "v2.0"

# Block / unblock
gh issue edit {number} --add-label "blocked"
gh issue edit {number} --remove-label "blocked"

Comment

gh issue comment {number} --body "{message}"

# Phase lifecycle comments (standard format)
gh issue comment {number} --body "Phase {N} {NAME}: starting. Target artifacts: {paths}"
gh issue comment {number} --body "Phase {N} {NAME}: complete. Artifacts: {paths}"
gh issue comment {number} --body "RED: Failing test at {path} — {message}"
gh issue comment {number} --body "GREEN: {count} tests passing."
gh issue comment {number} --body "BLOCKED: {agent} failed 3x on {task}. Human required."

Close

gh issue close {number} --comment "Acceptance criteria passing. Validation: {summary}"
gh issue close {number} --reason "not planned"

Pull Requests

# Create PR (always draft first)
gh pr create \
  --title "feat: {description}" \
  --body "## Summary\n{summary}\n\nCloses #{issue_number}" \
  --base main \
  --draft

# Mark ready when all gates pass
gh pr ready {number}

# View PR + CI status
gh pr view {number} --json number,title,state,reviews,statusCheckRollup,files

# Request review
gh pr edit {number} --add-reviewer "{login}"

# Check CI on PR
gh pr checks {number}

# Merge (squash — preferred)
gh pr merge {number} --squash --subject "feat: {description} (#{issue_number})" --delete-branch

# Review
gh pr review {number} --approve --body "LGTM"
gh pr review {number} --request-changes --body "{feedback}"

Branches

# List branches
gh api repos/{owner}/{repo}/branches --jq '.[].name'

# Create via API (when not in git context)
gh api repos/{owner}/{repo}/git/refs \
  -f ref="refs/heads/feat/{slug}" \
  -f sha="$(gh api repos/{owner}/{repo}/git/ref/heads/main --jq '.object.sha')"

# Delete remote branch
gh api -X DELETE repos/{owner}/{repo}/git/refs/heads/feat/{slug}

# Branch protection status
gh api repos/{owner}/{repo}/branches/main/protection

Actions / CI

# List recent runs
gh run list --limit 10
gh run list --workflow ci.yml --branch main --limit 5

# Watch a run live
gh run watch {run-id}

# View failed run details
gh run view {run-id} --log-failed

# Trigger workflow manually
gh workflow run {workflow-file} --ref main

# Re-run failed jobs only
gh run rerun {run-id} --failed

# Download artifact
gh run download {run-id} --name {artifact-name} --dir ./artifacts

Repo Inspection

# Full repo metadata
gh repo view --json name,owner,description,topics,visibility,defaultBranchRef,\
licenseInfo,stargazerCount,forkCount,createdAt,updatedAt

# Collaborators
gh api repos/{owner}/{repo}/collaborators --jq '.[].login'

# Repo settings
gh api repos/{owner}/{repo} --jq '{private:.private,hasIssues:.has_issues,hasWiki:.has_wiki}'

# Topics
gh repo edit --add-topic "{topic}"

# Clone URL
gh repo view --json sshUrl,url

# Releases
gh release list --limit 5
gh release view {tag}

Labels & Milestones

# Bootstrap MAPLE labels (idempotent)
bash scripts/bootstrap-labels.sh

# List labels
gh label list

# Create label
gh label create "type:task" --color "0075ca" --description "Work item"

# Create milestone
gh api repos/{owner}/{repo}/milestones \
  -f title="v1.0" \
  -f description="First release" \
  -f due_on="2026-06-01T00:00:00Z"

# List milestones
gh api repos/{owner}/{repo}/milestones --jq '.[] | {number:.number,title:.title,open:.open_issues}'

Search

# Search code in repo
gh search code "{query}" --repo {owner}/{repo} --json path,url

# Search issues across GitHub
gh search issues "{query}" --repo {owner}/{repo} --json number,title,state

# Search PRs
gh search prs "{query}" --repo {owner}/{repo} --state open

Issue Lifecycle (MAPLE Standard)

Event Label added Label removed Comment
Story created phase:discover "Phase 1 DISCOVER: story created."
Architect done phase:architect phase:discover "Phase 2 ARCHITECT: design complete."
Plan done phase:plan phase:architect "Phase 3 PLAN: plan.md ready."
Infra done phase:infra phase:plan "Phase 4 INFRA: containers healthy."
Implement start phase:implement,in-progress phase:infra "Phase 5 IMPLEMENT: TDD loop starting."
TDD red tdd:red "RED: failing test at {path}"
TDD green tdd:green tdd:red "GREEN: {n} tests passing."
Validate pass phase:validate,validated phase:implement "Phase 6 VALIDATE: 100% pass."
Document done phase:document phase:validate "Phase 7 DOCUMENT: docs complete."
PR created ready-for-review phase:document "Phase 8: PR #{pr} created."
Blocked blocked "BLOCKED: {agent} failed 3x. Human needed."
Unblocked blocked "Unblocked: {resolution}"

Failure Modes

Condition Action
Not authenticated gh auth login — do not retry ops
Label missing Create via gh label create before using
Milestone missing Create via gh api milestones before using
Rate limited Wait 60s, retry once; if fails again, stop
Issue already exists gh issue list --search "{title}" --state all — return existing number
gh not found Stop. Output: "gh CLI not available. Install from https://cli.github.com"
用于去除文本中的AI生成痕迹和模式化表达,使文档、注释、提交信息等内容更自然人性化。支持基于个人写作风格校准,检测29种AI特征,适用于合并前的最终润色。
需要去除AI写作痕迹时 提交代码前检查提交信息和注释 生成文档后润色README或变更日志 让AI辅助生成的文本听起来更自然
.cursor/skills/humanizer/SKILL.md
npx skills add kinncj/Heimdall --skill humanizer -g -y
SKILL.md
Frontmatter
{
    "name": "humanizer",
    "description": "Remove AI-isms and artificial language patterns from text. Makes documentation, comments, commit messages, and prose sound more natural and human. Based on Wikipedia's \"Signs of AI writing\" patterns."
}

humanizer skill

Polish text by removing detectable AI-generated writing patterns. Ideal for finalizing documentation, commit messages, comments, and any prose before merging.

When to use

  • After @docs generates feature documentation
  • Before committing code (check commit messages and comments)
  • Polishing README updates, CHANGELOG entries, or runbooks
  • Making AI-assisted prose sound more natural
  • Manual voice calibration for brand consistency

How to invoke

/humanizer

[paste your text here]

Or ask directly in chat:

Please humanize this text: [your text]

Voice calibration (optional)

Provide a sample of your own writing for the skill to match your style:

/humanizer

Here's a sample of my writing for voice matching:
[paste 2-3 paragraphs of your own writing]

Now humanize this text:
[paste AI text to humanize]

What it detects

The skill checks for 29 AI-writing patterns, including:

  • Significance inflation — "marking a pivotal moment in the evolution of..." → "was established in 1989"
  • Notability name-dropping — Lists of citations → specific examples with context
  • Superficial -ing phrases — "symbolizing, reflecting, showcasing" → active explanations
  • Promotional language — Corporate jargon → straightforward description
  • Overuse of 'indeed' — Filler words → tighter prose
  • Transition abuse — Excessive "Furthermore, In conclusion..." → natural flow
  • Hedging redundancy — "arguably, in some sense, it could be argued..." → clarity
  • Rare word stacking — Thesaurus abuse → common vocabulary
  • False expert mode — Generic expertise → grounded examples
  • Generalist fluff — "many fields, various domains..." → specific scope

Integration with MAPLE

  • Auto-call after Phase 7 (DOCUMENT) if humanize: true in feature story frontmatter
  • Manual: invoke at any phase before merge
  • Works across all harnesses (Claude Code, OpenCode, Cursor, Copilot CLI)

Output

Returns a humanized version of your text with:

  1. First pass: Remove identified AI patterns
  2. Final audit: "Obviously AI generated?" check
  3. Second rewrite: Catch lingering AI-isms

Further reading

提供Jupyter Notebook最佳实践,规范单元格顺序(从设置到结论),支持通过Papermill参数化执行和nbformat编程创建。涵盖导出、Git清理输出及确保可复现性的规则,适用于.ipynb文件处理。
处理 .ipynb 文件 优化 Jupyter Notebook 结构 配置 Papermill 参数化执行 使用 nbformat 创建笔记本 执行 notebook 导出或清理
.cursor/skills/jupyter-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill jupyter-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "jupyter-patterns",
    "description": "Apply best practices for Jupyter notebooks: cell ordering, reproducibility, parameterisation. Use when working with .ipynb files."
}

SKILL: Jupyter Notebook Patterns

Notebook Structure

Cells should follow this order:

  1. Setup — imports, configuration, constants
  2. Data Loading — load raw data with validation
  3. EDA — exploratory data analysis, distributions, correlations
  4. Preprocessing — cleaning, feature engineering
  5. Modeling — model training and evaluation
  6. Visualization — charts and figures
  7. Conclusions — findings summary, next steps

Parameterized Execution with Papermill

# In notebook cell tagged with "parameters":
# Click: View -> Cell Toolbar -> Tags -> add "parameters" tag

dataset = "data/train.csv"  # papermill will override this
output_dir = "outputs"
n_estimators = 100
random_state = 42
# Execute with papermill
papermill input.ipynb output.ipynb \
  -p dataset "data/test.csv" \
  -p n_estimators 200 \
  -p random_state 0

Programmatic Notebook Creation

import nbformat as nbf

nb = nbf.v4.new_notebook()
nb.cells = [
    nbf.v4.new_markdown_cell("# Analysis: {Title}"),
    nbf.v4.new_code_cell("import pandas as pd\nimport numpy as np"),
    nbf.v4.new_code_cell("df = pd.read_csv('data.csv')\ndf.head()"),
]

with open('analysis.ipynb', 'w') as f:
    nbf.write(nb, f)

Export

# To HTML (with outputs)
jupyter nbconvert --to html --execute notebook.ipynb

# To PDF
jupyter nbconvert --to pdf --execute notebook.ipynb

# Execute in place
jupyter nbconvert --to notebook --execute --inplace notebook.ipynb

Git Hygiene

# Clear outputs before commit
jupyter nbconvert --to notebook --ClearOutputPreprocessor.enabled=True \
  --inplace notebook.ipynb

# Or use nbstripout (installs as git filter)
pip install nbstripout
nbstripout --install

Rules

  • Restart kernel and run all cells before committing.
  • Clear all outputs before git commit.
  • Tag parameter cells for papermill.
  • Each notebook should be self-contained and reproducible.
  • Include random_state parameter for reproducibility.
  • Use relative paths for data files.
基于 Karpathy 四大原则(思考、简洁、精准、目标驱动)审计代码变更。在实施阶段后自动触发或手动调用,生成合规报告并依据分数决定流程是否推进。
Phase 5 IMPLEMENT 完成后自动触发 用户手动输入 /karpathy-audit 或 @karpathy-audit
.cursor/skills/karpathy-audit/SKILL.md
npx skills add kinncj/Heimdall --skill karpathy-audit -g -y
SKILL.md
Frontmatter
{
    "name": "karpathy-audit",
    "description": "Audit code changes against Karpathy's 4 principles (Think Before Coding, Simplicity First, Surgical Changes, Goal-Driven Execution). Auto-called after Phase 5 IMPLEMENT; can also be invoked manually. Produces scored compliance report and blocks advancement if threshold not met."
}

karpathy-audit skill

Audit PRs and code changes against Andrej Karpathy's 4 principles for reducing LLM coding mistakes.

When to use

  • Auto-invoked after Phase 5 (IMPLEMENT) before advancing to Phase 6 (VALIDATE)
  • Manual invocation: /karpathy-audit or @karpathy-audit in chat
  • At any phase when you want to assess code quality against the principles

The 4 Principles

1. Think Before Coding

Don't assume. Don't hide confusion. Surface tradeoffs.

  • State assumptions explicitly. If uncertain, ask.
  • Present multiple interpretations—don't pick silently.
  • If a simpler approach exists, say so. Push back when warranted.
  • Stop when confused. Name what's unclear and ask.

2. Simplicity First

Minimum code that solves the problem. Nothing speculative.

  • No features beyond what was asked.
  • No abstractions for single-use code.
  • No "flexibility" or "configurability" that wasn't requested.
  • No error handling for impossible scenarios.
  • If 200 lines could be 50, rewrite it.

3. Surgical Changes

Touch only what you must. Clean up only your own mess.

  • Don't "improve" adjacent code, comments, or formatting.
  • Don't refactor things that aren't broken.
  • Match existing style, even if you'd do it differently.
  • Only remove imports/variables/functions that YOUR changes made unused.

4. Goal-Driven Execution

Define success criteria. Loop until verified.

  • Transform tasks into verifiable goals: tests before code.
  • State a brief plan with explicit success criteria.
  • Loop independently until verified.

How to invoke

Auto-call (Phase 5 → Phase 6 gate): Orchestrator automatically calls this skill after IMPLEMENT phase to audit the diff.

Manual call:

/karpathy-audit

or

@karpathy-audit

Audit output

The skill produces a compliance report written to .claude/state/karpathy-report.json:

{
  "phase": "IMPLEMENT",
  "timestamp": "2026-05-18T21:25:00Z",
  "spec_file": "docs/stories/user-auth/Story.md",
  "audit": {
    "think_before_coding": {
      "score": 85,
      "violations": ["Assumed DB schema without asking"],
      "evidence": ["Line 42: const User = model('User')"]
    },
    "simplicity_first": {
      "score": 92,
      "violations": [],
      "evidence": ["Reduced 180→120 lines in refactor"]
    },
    "surgical_changes": {
      "score": 78,
      "violations": ["Modified logging in unrelated file"],
      "scope_creep_files": ["src/logging/index.ts"],
      "out_of_spec": true
    },
    "goal_driven": {
      "score": 100,
      "violations": [],
      "evidence": ["All tests passing. RED→GREEN→REFACTOR complete."]
    },
    "overall": 89,
    "gate_decision": "PASS_WITH_APPROVAL",
    "recommendation": "Score 89: Pass scope and simplicity checks. Recommend human approval before advancing to Phase 6."
  }
}

Scoring & Gate Decisions

Overall Score Decision Action
≥90 🟢 PASS Auto-advance to next phase
70-89 🟡 PASS_WITH_APPROVAL Pause. Require human approval to advance.
<70 🔴 FAIL BLOCK. Orchestrator halts. Requires remediation + re-audit.

Scope creep detection

Compares two sources:

  1. Original spec — From docs/stories/{story}/Story.md (required scope)
  2. PR diff — Files and line changes in the current PR

Violations flagged:

  • Files modified outside the spec boundary
  • Feature branches added beyond the story requirement
  • Unrelated cleanup/refactoring

Phase 5 → Phase 6 Gate

Orchestrator behavior:

Phase 5 (IMPLEMENT) complete
  ↓
Auto-call: /karpathy-audit
  ↓
Analyze spec + PR diff + 4 principles
  ↓
Write `.claude/state/karpathy-report.json`
  ↓
Score ≥90?      → Auto-advance to Phase 6
Score 70-89?    → Pause. Display report. Require [a]pprove or [c]cancel
Score <70?      → HALT. Show violations. Require /karpathy-audit re-run after fixes

Dashboard integration

The TUI displays:

  • Phase 5 detail view — Shows karpathy-report.json if present
  • [P] overlay (Pipeline pane) — Per-principle scores + violations
  • Red blocking indicator — If score <70, blocks manual phase advance
  • Approval gate — If 70-89, shows "awaiting approval" state

Remediation workflow

If audit fails or scores <70:

1. Orchestrator shows violation details
2. Specialist agent re-assesses work against the principle
3. Fix or remove out-of-scope code
4. Manually re-run: /karpathy-audit
5. If all 4 principles ≥70 now, approve advancement

Example invocation

User: /feature "add password reset via email"
Orchestrator: [Phase 1-5 complete]
Orchestrator: Auditing against Karpathy principles...
karpathy-audit: Running audit...
[Analyze spec vs PR diff]
[Score each principle]
karpathy-audit: 📋 Report written to .claude/state/karpathy-report.json
karpathy-audit: 🟡 Score 82/100 — PASS_WITH_APPROVAL
karpathy-audit: ⚠️ Scope creep: modified src/logging (out of spec)

Dashboard: [Shows approval gate, awaiting [a] key]
User: a  [approve]
Dashboard: [Phase 6 VALIDATE begins]

Further reading

提供Kubernetes和Kustomize的最佳实践模板,涵盖部署、服务及环境覆盖层结构。包含Deployment、PDB、HPA的标准YAML配置及安全规范,并附带验证工作流指导,用于编写或审查K8s清单文件。
编写Kubernetes资源文件 审查K8s YAML配置 使用Kustomize管理多环境配置 需要PodDisruptionBudget或HPA配置
.cursor/skills/kubernetes-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill kubernetes-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "kubernetes-patterns",
    "description": "Apply Kubernetes and Kustomize patterns for deployments, services, and overlays. Use when writing or reviewing k8s manifests."
}

SKILL: Kubernetes Patterns

Kustomize Structure

infra/k8s/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── patches/
    ├── staging/
    │   ├── kustomization.yaml
    │   └── patches/
    └── prod/
        ├── kustomization.yaml
        └── patches/

Deployment with Required Fields

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {app-name}
  labels:
    app: {app-name}
spec:
  replicas: 2
  selector:
    matchLabels:
      app: {app-name}
  template:
    metadata:
      labels:
        app: {app-name}
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
      containers:
        - name: {app-name}
          image: {image}:{tag}
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 20
          startupProbe:
            httpGet:
              path: /health
              port: 3000
            failureThreshold: 30
            periodSeconds: 10

PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {app-name}-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: {app-name}

HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {app-name}-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {app-name}
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Validation Workflow

# Always validate before applying
kubectl apply --dry-run=client -f {manifest}
kustomize build overlays/dev | kubectl apply --dry-run=client -f -
helm template {release} {chart} | kubectl apply --dry-run=client -f -
用于生成和验证组件、序列、ER、部署及状态机Mermaid图表,辅助架构文档化。需遵循节点数限制并确保语法有效,提交前应在Live Editor测试。
需要为功能特性生成架构图 记录系统组件交互关系 设计数据库实体模型 绘制服务部署拓扑 描述业务状态流转
.cursor/skills/mermaid-diagrams/SKILL.md
npx skills add kinncj/Heimdall --skill mermaid-diagrams -g -y
SKILL.md
Frontmatter
{
    "name": "mermaid-diagrams",
    "description": "Generate and validate Mermaid diagrams (component, sequence, ER, deployment, state machine) for features. Use when documenting architecture."
}

SKILL: Mermaid Diagrams

Required Diagrams Per Feature

  1. Component diagram (graph TB)
  2. Sequence diagram (sequenceDiagram)
  3. ER diagram (erDiagram)
  4. Deployment diagram (graph TB with deployment focus)
  5. State machine (stateDiagram-v2) — where applicable

Rules

  • Maximum 30 nodes per diagram.
  • All diagrams must be syntactically valid.
  • Test diagrams in the Mermaid Live Editor before committing.
  • Use descriptive node labels.

Component Diagram

graph TB
    subgraph "Frontend"
        UI["React App"]
    end
    subgraph "Backend"
        API["REST API"]
        DB[("PostgreSQL")]
        Cache[("Redis")]
    end
    UI -->|"HTTPS"| API
    API --> DB
    API --> Cache

Sequence Diagram

sequenceDiagram
    actor U as User
    participant F as Frontend
    participant A as API
    participant D as Database

    U->>F: Action
    F->>A: POST /api/resource
    A->>D: INSERT
    D-->>A: 201 Created
    A-->>F: {id, data}
    F-->>U: Success

ER Diagram

erDiagram
    USER {
        uuid id PK
        string email UK
        timestamp created_at
    }
    ORDER {
        uuid id PK
        uuid user_id FK
        decimal total
        timestamp created_at
    }
    USER ||--o{ ORDER : "places"

State Machine

stateDiagram-v2
    [*] --> Pending
    Pending --> Processing: payment received
    Processing --> Completed: fulfilled
    Processing --> Failed: error
    Failed --> Pending: retry
    Completed --> [*]

Deployment Diagram

graph TB
    subgraph "Vercel Edge"
        FE["Next.js App"]
        EF["Edge Functions"]
    end
    subgraph "AWS"
        LB["Load Balancer"]
        APP["App Servers"]
        RDS[("RDS PostgreSQL")]
        EC["ElastiCache Redis"]
    end
    FE --> EF
    EF --> LB
    LB --> APP
    APP --> RDS
    APP --> EC
根据已批准的原型图和令牌,使用项目指定的UI栈(如Mantine、Tailwind或HTML)生成高保真UI组件Mockup代码及元数据。支持Web和Tui目标,需通过预检确保原型图状态和令牌文件有效。
实现已批准的原型图 需要基于设计令牌生成UI组件代码
.cursor/skills/mockup/SKILL.md
npx skills add kinncj/Heimdall --skill mockup -g -y
SKILL.md
Frontmatter
{
    "name": "mockup",
    "description": "Scaffold high-fidelity UI component mockups using the project UI stack consuming approved wireframes. Use when implementing approved wireframes."
}

SKILL: mockup

Purpose

Scaffold high-fidelity UI component mockups using the project's declared UI stack (Mantine, Tailwind/shadcn, or plain HTML). Mockups consume approved wireframes and design tokens. Output is runnable code — not a design file. Human approval required before the mockup is treated as the implementation target.

Inputs

Field Source Example
story_file path to story markdown docs/stories/auth-reset-0001.md
wireframe_file path to approved wireframe docs/design/wireframes/auth-reset-0001.wireframe.md
tokens_file canonical token file docs/design/identity/tokens.json
stack project.config.yaml react-mantine | react-tailwind | html

Supported Stacks (web target)

Stack value Framework Token source
react-mantine @mantine/core mantine.theme.ts
react-tailwind Tailwind CSS + shadcn/ui tailwind.tokens.js
react-shadcn shadcn/ui + Tailwind tailwind.tokens.js
html Vanilla HTML + tokens.css tokens.css

Outputs

File Location Targets
<story-id>.mockup.md docs/design/mockups/ web + tui — metadata + approval (tui: also the terminal render + lipgloss styles)
<story-id>.mockup.tsx docs/design/mockups/ web only — React stacks
<story-id>.mockup.html docs/design/mockups/ web only — html stack

Target awareness

design.target (project.config.yaml, default web):

  • web — generate the code mockup (.tsx/.html) for the configured ui_library, plus .mockup.md.
  • tui — generate ONLY <id>.mockup.md: a fenced monospace terminal render at the target width covering default/selected/empty/error/loading states, followed by a "Styles" section mapping each region to lipgloss Foreground/Background/Border/Padding taken from docs/design/identity/terminal-theme.json. Skip the stack detector and the React templates below.

Pre-flight Checks

Before generating:

WIREFRAME="docs/design/wireframes/${STORY_ID}.wireframe.md"
TOKENS="docs/design/identity/tokens.json"

# 1. Wireframe must be approved
STATUS=$(python3 -c "
import re
m = re.search(r'^status:\s*(\w+)', open('$WIREFRAME').read(), re.MULTILINE)
print(m.group(1) if m else 'draft')
")
[ "$STATUS" = "approved" ] || { echo "BLOCKED: wireframe not approved"; exit 1; }

# 2. Tokens must exist
[ -f "$TOKENS" ] || { echo "BLOCKED: tokens.json missing — run visual-identity skill first"; exit 1; }

Mockup Template: react-mantine

// AUTO-GENERATED MOCKUP — docs/design/mockups/{story-id}.mockup.tsx
// Story: {story title}
// Wireframe: {wireframe file}
// Tokens: mantine.theme.ts
// Status: draft — awaiting approval

import { MantineProvider, Stack, TextInput, Button, Text, Alert } from '@mantine/core';
import { theme } from '../../docs/design/identity/mantine.theme';
import { IconAlertCircle } from '@tabler/icons-react';

interface Props {
  error?: string;
  onSubmit: (email: string) => void;
}

export function {ComponentName}({ error, onSubmit }: Props) {
  const [email, setEmail] = useState('');

  return (
    <MantineProvider theme={theme}>
      <Stack gap="md" maw={400} mx="auto" mt="xl">
        <Text fw={700} size="xl">{Screen Title}</Text>

        <TextInput
          label="Email"
          placeholder="you@example.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          error={error}
        />

        {error && (
          <Alert icon={<IconAlertCircle />} color="red" variant="light">
            {error}
          </Alert>
        )}

        <Button fullWidth onClick={() => onSubmit(email)}>
          Send Reset Link
        </Button>
      </Stack>
    </MantineProvider>
  );
}

Mockup Template: react-tailwind

// AUTO-GENERATED MOCKUP — docs/design/mockups/{story-id}.mockup.tsx
import { useState } from 'react';

interface Props {
  error?: string;
  onSubmit: (email: string) => void;
}

export function {ComponentName}({ error, onSubmit }: Props) {
  const [email, setEmail] = useState('');

  return (
    <div className="max-w-sm mx-auto mt-16 space-y-4">
      <h1 className="text-2xl font-bold text-foreground">{Screen Title}</h1>

      <div>
        <label className="block text-sm font-medium mb-1">Email</label>
        <input
          type="email"
          className={`w-full border rounded-md px-3 py-2 text-sm ${error ? 'border-error' : 'border-border'}`}
          placeholder="you@example.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        {error && <p className="text-error text-sm mt-1">{error}</p>}
      </div>

      <button
        className="w-full bg-brand-primary text-white rounded-md py-2 font-medium hover:opacity-90"
        onClick={() => onSubmit(email)}
      >
        Send Reset Link
      </button>
    </div>
  );
}

Mockup Metadata File

Always write a .mockup.md alongside the code file:

---
story_id: "{story_id}"
wireframe: "docs/design/wireframes/{story_id}.wireframe.md"
stack: "{stack}"
status: draft        # draft | approved | rejected
approved_by: null
approved_at: null
---

## Mockup: {story title}

### States

- Default
- Error: {describe}
- Success: {describe}
- Loading: {describe if applicable}

### Token Usage

- Primary action: `color.brand.primary`
- Error text: `color.semantic.error`
- Border: `color.surface.border`

### Approval

- [ ] Matches approved wireframe
- [ ] Design tokens applied correctly
- [ ] All interaction states present
- [ ] Approved by product owner / UX lead

Failure Modes

Condition Action
Wireframe not approved Stop. Output BLOCKED. Do not generate mockup.
tokens.json missing Stop. Run visual-identity first.
Stack unknown Default to html. Log warning.
Mockup exists and is approved Do not overwrite. Log SKIP — approved mockup locked.
Component name collision Append story ID suffix to component name.

Logging

[mockup] CREATE   docs/design/mockups/auth-reset-0001.mockup.tsx  stack=react-mantine
[mockup] CREATE   docs/design/mockups/auth-reset-0001.mockup.md   status=draft
[mockup] BLOCKED  auth-reset-0001  wireframe not approved
[mockup] SKIP     auth-reset-0001  mockup approved — locked
统一调度器,用于执行Taffy工作流、本地技能或子代理。支持从skills.sh注册表自动安装缺失技能,并在运行过程中实时更新进度状态至maple.json,兼容多种AI助手环境及Gherkin规范约束。
需要执行特定命名的工作流或任务 调用未本地安装的远程技能 启动多阶段自动化流程
.cursor/skills/pipeline-runner/SKILL.md
npx skills add kinncj/Heimdall --skill pipeline-runner -g -y
SKILL.md
Frontmatter
{
    "name": "pipeline-runner",
    "description": "Universal dispatcher: run a named taffy workflow (.claude\/taffy\/<name>.yaml), a skill (\/skill-name), or a sub-agent (@agent-name). Falls back to skills.sh registry when a skill is not found locally. Tracks all runs in .claude\/state\/maple.json so the maple TUI shows live progress."
}

SKILL: pipeline-runner

What It Does

Dispatches any named workflow, skill, or agent from a single entry point. Resolution order:

  1. Taffy workflow — look for .claude/taffy/<name>.yaml; if found, execute each stage in order
  2. Skill (local) — look for .claude/skills/<name>/; if found, invoke the skill
  3. Agent — look for .claude/agents/<name>.md; if found, delegate to @<name>
  4. Skill (registry) — if not found locally, try to install from skills.sh, then retry step 2

Pipeline state is written to .claude/state/maple.json at every transition.

Usage

/pipeline-runner <name>

Examples:

/pipeline-runner new-ui-feature
/pipeline-runner api-endpoint
/pipeline-runner implement-stories
/pipeline-runner tdd-workflow
/pipeline-runner orchestrator

List available taffy workflows:

ls .claude/taffy/*.yaml | grep -v schema

List available skills:

ls .claude/skills/

Dispatch Protocol

Step 1: Resolve the target

# Check taffy first
[ -f ".claude/taffy/<name>.yaml" ] && dispatch=taffy
# Then local skill
[ -d ".claude/skills/<name>" ] && dispatch=skill
# Then agent
[ -f ".claude/agents/<name>.md" ] && dispatch=agent
# Fallback: fetch from skills.sh registry, then retry
if [ -z "$dispatch" ] && command -v npx &>/dev/null; then
  echo "pipeline-runner: '<name>' not found locally — checking skills.sh…"
  npx --yes skills add kinncj/maple@<name> -a claude-code -y 2>/dev/null \
    || npx --yes skills add <name> -a claude-code -y 2>/dev/null \
    || true
  [ -d ".claude/skills/<name>" ] && dispatch=skill
fi

If nothing matches after the registry fallback, report: pipeline-runner: no taffy workflow, skill, or agent named '<name>' (also checked skills.sh registry)

Step 2: Initialise state

Write to .claude/state/maple.json (merge — do not overwrite unowned fields):

{
  "taffy": "<name>",
  "stage": "<first-stage or skill-name>",
  "status": "RUNNING",
  "awaiting_approval": null,
  "started_at": "<iso8601>",
  "updated_at": "<iso8601>"
}

Step 2b: Runtime policy enforcement (mandatory)

Before any stage execution, read and enforce:

  • Harness-specific root markdown:
    • Claude harness → CLAUDE.md
    • OpenCode harness → OPENCODE.md
    • Cursor harness → CURSOR.md
    • Copilot harness → COPILOT.md
  • AGENTS.md
  • .github/copilot-instructions.md
  • .github/instructions/stories.instructions.md (when touching story files)

When the launch prompt contains a <maple-gherkin-handoff> block:

  1. Treat it as hard scope: implement only listed story paths / IDs.
  2. Do not run Spec-Kit or regenerate stories.
  3. Preserve the repository's current Cucumber stack:
    • If generated stories include cucumber/*_steps.py, use Python behave-style steps.
    • Do not introduce TypeScript @cucumber/cucumber unless the repository already uses it as the active standard.
  4. Keep BusinessRepo structure and phase gates exactly as defined by instruction files.
  5. Treat test layout as mandatory:
    • Gherkin feature files must live under /tests/features/.
    • Step definitions must use the repository's active Cucumber stack and live under /tests.
    • Do not place acceptance tests under /app or story directories.
  6. Enforce module boundaries independent of language:
    • Runtime/source files must not import from docs/, .github/, or .claude/.
    • Copying/adapting approved artifact content into app/test source is allowed.
    • Design/spec artifacts are references, not runtime code dependencies.

Step 3a: Taffy workflow execution

Load .claude/taffy/<name>.yaml, parse stages:, resolve depends_on order.

If the workflow defines an orchestrator-kickoff stage, execute it first before all other stages. This stage must publish the initial plan and heartbeat cadence.

For each stage:

when: guard:

  • when: ui:true — skip if story has ui: false, unless .claude/state/maple.json has force_ui: true with launch_source: maple-x (MAPLE [x] quick-launch override).
  • when: ui:false — skip if story has ui: true
  • when: always — always run
  • UI-related scope includes web, mobile, desktop, and TUI stories. Any such run must include design-review human-approval stages before implementation phases.

depends_on: all listed stages must be DONE before this one starts.

Dispatch:

  • agent: <name> → delegate to @<name> with current story context
  • skill: <name> → invoke the skill
  • pipeline: standard → run the full 8-phase orchestrator pipeline

After each stage: update maple.json with current stage + RUNNING.

Progress heartbeats (mandatory):

  • Send an immediate kickoff status before the first long-running tool/agent call.
  • While a taffy run is active, send a concise progress update at least every 60-120 seconds.
  • On each heartbeat, refresh maple.json updated_at and current stage.
  • Every heartbeat must include concrete progress evidence:
    • changed files/artifacts since last update (explicit paths), or
    • a specific blocker that prevented changes.
  • Use this status format:
    • Progress: <stage / phase>
    • Done since last update: <brief>
    • Current action: <brief>
    • Blockers: <none or blocker>
    • Next update: <ETA>
  • Do not send heartbeat-only timestamp churn with no artifact/blocker details.
  • If a stage requires writing artifacts and write access/tools are unavailable, set maple.json to FAILED with an explicit error and stop.
  • If blocked/waiting, report what is pending and continue heartbeats until unblocked.

Completion artifact gate (mandatory):

  • Before marking DONE, verify the run produced concrete story-linked artifacts under the BusinessRepo layout.
  • Required for implementation runs:
    • application changes in /app (or existing domain folders),
    • tests in /tests (unit/integration/e2e as applicable),
    • Gherkin assets in /tests/features plus matching step implementations.
  • Boundary check:
    • fail the run if generated runtime code imports paths under docs/, .github/, or .claude/.
  • If required test/gherkin artifacts are missing, set maple.json to FAILED and report missing paths explicitly.

Step 3b: Skill invocation

Invoke the skill directly. Update maple.json on start and completion.

Step 3c: Agent delegation

Delegate to @<name>. Update maple.json on start and completion.

Step 4: Human-approval gates (taffy only)

When a stage has gate: human-approval:

  1. Complete stage work (produce artifact).
    • For design review stages (wireframe, visual-identity, design-tokens, ui-mockup-builder, design-refresh), artifact production is mandatory:
      • create at least one previewable artifact (.excalidraw, .html, .svg, .png, .jpg, .jpeg, .webp, or .md) under docs/design (or approved artifact dirs), and
      • the required formats depend on design.target (project.config.yaml, default web): web = .md + .html + .excalidraw; tui = .md + .excalidraw (no .html). A run that produces fewer than the required formats is incomplete, and
      • update .claude/state/design-artifacts.json with current stage artifact paths so the review portal can update live.
    • Path + completeness gate for wireframe stage (mandatory before PAUSING):
      # Verify wireframes are in the canonical location and the required formats exist.
      # Required formats depend on design.target (web: md+html+excalidraw; tui: md+excalidraw).
      TARGET=$(sed -n 's/^[[:space:]]*target:[[:space:]]*\([a-z]*\).*/\1/p' project.config.yaml 2>/dev/null | head -1)
      [ -z "$TARGET" ] && TARGET=web
      CANONICAL=$(find docs/design/wireframes -name "*.wireframe.*" 2>/dev/null | wc -l | tr -d ' ')
      MISPLACED=$(find docs -name "*.wireframe.*" -not -path "*/docs/design/wireframes/*" 2>/dev/null)
      MISSING_HTML=""
      if [ "$TARGET" != "tui" ]; then
        MISSING_HTML=$(find docs/design/wireframes -name "*.wireframe.md" 2>/dev/null | while read md; do
          base="${md%.wireframe.md}"
          [ ! -f "${base}.wireframe.html" ] && echo "${base}.wireframe.html"
        done)
      fi
      MISSING_EXCALIDRAW=$(find docs/design/wireframes -name "*.wireframe.md" 2>/dev/null | while read md; do
        base="${md%.wireframe.md}"
        [ ! -f "${base}.wireframe.excalidraw" ] && echo "${base}.wireframe.excalidraw"
      done)
      if [ "$CANONICAL" -eq 0 ]; then
        if [ -n "$MISPLACED" ]; then
          echo "PIPELINE GATE FAILED: wireframes found at wrong path(s): $MISPLACED"
          echo "Canonical path is docs/design/wireframes/ — move files there before this stage can complete."
        else
          echo "PIPELINE GATE FAILED: no wireframe artifacts in docs/design/wireframes/"
        fi
        # set maple.json FAILED and stop
      fi
      if [ -n "$MISSING_HTML" ]; then
        echo "PIPELINE GATE FAILED: missing .wireframe.html for: $MISSING_HTML — generate it now."
        # set maple.json FAILED and stop
      fi
      if [ -n "$MISSING_EXCALIDRAW" ]; then
        echo "PIPELINE GATE FAILED: missing .wireframe.excalidraw for: $MISSING_EXCALIDRAW — generate it now."
        # set maple.json FAILED and stop
      fi
      
      If the gate fails, produce the missing files before re-running the gate check.
    • If no reviewable artifact exists for a design gate, set maple.json to FAILED and stop.
  2. Write PAUSED state:
{ "stage": "<name>", "status": "PAUSED", "awaiting_approval": "<name>", "updated_at": "<iso8601>" }
  1. Write stage name to .claude/state/approval-pending.txt.
  2. Output:
TAFFY PAUSED — awaiting human approval
Stage:    <stage-name>
Artifact: <artifact path or description>

Approve via the maple TUI ([P] pipeline → [a] approve) or reply "approved" / "continue".
I will not advance to the next stage until approval is confirmed.
  1. Poll: timeout 540 bash -c 'until [ ! -f .claude/state/approval-pending.txt ]; do sleep 2; done'
    • On timeout (exit 124), re-run the same poll. The Bash tool caps at 10 min per call; re-polling across calls lets approval delays exceed that bound.
    • Also accept an explicit "approved" / "continue" reply in chat.
    • When the user approves via the maple TUI ([P] → [a]), the TUI deletes the pending file and sends a "continue" keystroke to the agent's pane via the active multiplexer (outer tmux/zellij, or a detached tmux new-session wrapper). Either signal is sufficient to resume.
  2. On resume: update to RUNNING, advance to next stage.
  3. While paused, monitor .claude/state/design-feedback.json:
    • status: requested_changes or status: rejected means apply the requested updates before advancing.
    • Treat attachments as required review inputs (uploaded files such as .excalidraw, images, HTML, text), typically under docs/design/review-input/.
    • Summarize how each feedback item and attachment was addressed before continuing.

Step 5: Completion

{ "taffy": "<name>", "stage": "DONE", "status": "DONE", "awaiting_approval": null, "updated_at": "<iso8601>" }

Output:

TAFFY COMPLETE — <name>
Stages run: N
Duration:   <elapsed>

Failure Handling

After 3 consecutive failures on any stage:

{ "stage": "<name>", "status": "FAILED", "error": "<summary>", "updated_at": "<iso8601>" }

Stop. Report failed stage and error to human. Do not proceed.

Session Context

On startup, read .claude/state/sessions.json if it exists:

{ "claude": "<uuid>", "opencode": "<id>", "copilot": "<id>" }

Use the matching session ID when resuming work within an existing agent session.

State File Reference

All state in .claude/state/. TUI and skill share these files.

.claude/state/maple.json

Field Owner Values
taffy skill workflow/skill/agent name
stage skill current stage name
status skill RUNNING, PAUSED, DONE, FAILED
awaiting_approval skill local MAPLE stage name (e.g., spec-kit = Specification Knowledge & Integration Toolkit) or null — not an external package reference
pipeline skill standard if running 8-phase
started_at skill ISO 8601
updated_at skill ISO 8601
state TUI running or exited
ts TUI ISO 8601

Merge-not-overwrite: read existing file, update only owned fields, re-write.

.claude/state/approval-pending.txt

Skill writes stage name. TUI deletes when user presses [a].

.claude/state/sessions.json

TUI writes harness→session-ID map. Skill reads for session resume.

Skip Conditions

  • spike/* and chore/* branches: skip Spec-Kit stages, run implementation stages.
  • Stage when: ui:true on a ui: false story: skip silently, log [pipeline-runner] SKIP stage=<name> reason=ui:false unless quick-launch override is active (force_ui=true, launch_source=maple-x).
提供Playwright CLI交互探索、基于快照的页面操作及会话管理功能。支持将交互转换为测试代码,执行E2E测试及API请求验证,优化Token效率。
需要交互式浏览网页并获取元素快照 通过引用ID进行点击或填写表单 编写或运行浏览器自动化测试 记录用户操作生成测试脚本
.cursor/skills/playwright-cli/SKILL.md
npx skills add kinncj/Heimdall --skill playwright-cli -g -y
SKILL.md
Frontmatter
{
    "name": "playwright-cli",
    "description": "Use Playwright CLI for interactive browser exploration and snapshot-based interaction. Use when writing or running browser automation tests."
}

SKILL: Playwright CLI

When to Use

Use Playwright CLI (playwright-cli) for interactive browser exploration and snapshot-based interaction. Use npx playwright test for running actual test suites.

Installation

npm install -g @playwright/cli
npx playwright install  # installs browsers

Core CLI Commands

Browser Exploration

# Open browser to URL
playwright-cli open http://localhost:3000

# Take accessibility snapshot (YAML with element references)
playwright-cli snapshot
# Output: saves to disk as snapshot.yml — review before acting

# Interact by element reference (e.g., e21 from snapshot)
playwright-cli click e21
playwright-cli fill e35 "test@example.com"
playwright-cli press e35 Enter
playwright-cli screenshot  # saves to disk

Session Management

# Named sessions for auth state
playwright-cli --session=auth open http://localhost:3000/login
playwright-cli --session=auth fill e10 "admin@example.com"
playwright-cli --session=auth fill e11 "password"
playwright-cli --session=auth click e20  # submit button
playwright-cli session-list

Snapshot-to-Test Workflow

  1. playwright-cli open http://localhost:3000
  2. playwright-cli snapshot → review element refs in snapshot.yml
  3. Interact: playwright-cli click e21, playwright-cli fill e35 "value"
  4. Observe results
  5. Convert to test file:
// tests/e2e/feature.spec.ts
import { test, expect } from '@playwright/test';

test('should {outcome} when {action}', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Test Execution

# Run all E2E tests
npx playwright test tests/e2e/

# Run specific test file
npx playwright test tests/e2e/auth.spec.ts

# Visual debugger
npx playwright test --ui

# Headed mode (visible browser)
npx playwright test --headed

# Generate tests by recording
npx playwright codegen http://localhost:3000

# View HTML report
npx playwright show-report

API Testing (Playwright request fixture)

test('POST /api/users returns 201', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: {
      email: 'test@example.com',
      name: 'Test User'
    }
  });
  expect(response.status()).toBe(201);
  const body = await response.json();
  expect(body).toHaveProperty('id');
});

Token Efficiency Note

Use playwright-cli snapshot (saves to disk) instead of screenshots in context. The compact YAML element refs (~1,350 tokens) vs accessibility tree injection (~5,700 tokens).

提供PostgreSQL模式、迁移和查询的最佳实践,包括RLS配置、索引优化、PgBouncer连接池及幂等SQL规范,适用于数据库架构设计与迁移开发。
编写或修改PostgreSQL数据库表结构 生成或执行数据库迁移脚本 优化慢查询或设计数据库索引 配置PostgreSQL连接池
.cursor/skills/postgresql-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill postgresql-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "postgresql-patterns",
    "description": "Apply PostgreSQL schema, migration, and query patterns with idempotent SQL. Use when writing database schemas or migrations."
}

SKILL: PostgreSQL Patterns

Migration Naming

migrations/
  V001__create_users.sql          # Flyway
  0001_create_users.sql           # Generic
  20240115_create_users.sql       # Timestamp-based

Table Pattern with RLS

-- Create table
CREATE TABLE IF NOT EXISTS users (
  id          uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  email       text UNIQUE NOT NULL,
  name        text NOT NULL,
  role        text NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
  created_at  timestamptz DEFAULT now() NOT NULL,
  updated_at  timestamptz DEFAULT now() NOT NULL
);

-- Indexes
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_users_role ON users(role) WHERE role != 'user';

-- Updated_at trigger
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_updated_at
  BEFORE UPDATE ON users
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY users_select_own ON users
  FOR SELECT USING (id = current_user_id());

CREATE POLICY users_admin_all ON users
  FOR ALL USING (current_user_role() = 'admin');

Query Optimization

-- Always EXPLAIN ANALYZE in development
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > now() - interval '30 days'
GROUP BY u.id;

-- Partial index for common filter
CREATE INDEX idx_orders_pending
  ON orders(created_at)
  WHERE status = 'pending';

-- GIN index for full-text search
CREATE INDEX idx_products_search
  ON products USING gin(to_tsvector('english', name || ' ' || description));

Connection Pooling (PgBouncer)

# pgbouncer.ini
[databases]
app = host=127.0.0.1 port=5432 dbname=app

[pgbouncer]
pool_mode = transaction  # transaction pooling for most apps
max_client_conn = 100
default_pool_size = 20

Rules

  • NEVER modify existing migration files.
  • Always create new migration for schema changes.
  • Index all foreign keys.
  • Use CONCURRENTLY for index creation on large tables.
  • EXPLAIN ANALYZE all queries > 100ms.
  • Enable RLS on tables with user data.
提供Redis集成最佳实践,包括缓存旁路、限流及会话存储模式。定义键命名规范,强调设置TTL、使用SCAN和管道操作等规则,确保高性能与稳定性。
需要实现Redis缓存策略 构建API限流功能 配置Redis会话存储
.cursor/skills/redis-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill redis-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "redis-patterns",
    "description": "Apply Redis data structure, caching, and pub\/sub patterns. Use when integrating Redis into a service."
}

SKILL: Redis Patterns

Key Design Convention

{app}:{domain}:{entity}:{id}
# Examples:
myapp:user:session:usr_123
myapp:product:cache:prod_456
myapp:rate:api:ip_1.2.3.4
myapp:queue:emails:pending

Cache-Aside Pattern

async function getUser(id: string): Promise<User> {
  const key = `myapp:user:data:${id}`;
  const ttl = 3600; // 1 hour

  // Try cache first
  const cached = await redis.get(key);
  if (cached) {
    return JSON.parse(cached) as User;
  }

  // Cache miss — fetch from DB
  const user = await db.users.findById(id);
  if (!user) throw new Error('User not found');

  // Store in cache with TTL
  await redis.setex(key, ttl, JSON.stringify(user));
  return user;
}

// Invalidate on update
async function updateUser(id: string, data: Partial<User>): Promise<User> {
  const user = await db.users.update(id, data);
  await redis.del(`myapp:user:data:${id}`);
  return user;
}

Rate Limiting with Sorted Set

async function rateLimit(ip: string, limit: number, windowSeconds: number): Promise<boolean> {
  const key = `myapp:rate:api:${ip}`;
  const now = Date.now();
  const windowStart = now - (windowSeconds * 1000);

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart);
  pipeline.zadd(key, now, `${now}`);
  pipeline.zcard(key);
  pipeline.expire(key, windowSeconds);

  const results = await pipeline.exec();
  const count = results?.[2]?.[1] as number;
  return count <= limit;
}

Session Store

// Using ioredis with connect-redis
import session from 'express-session';
import RedisStore from 'connect-redis';

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24, // 24 hours
  }
}));

Rules

  • ALWAYS set TTLs on cache keys — never store indefinitely.
  • Use SCAN not KEYS in production (non-blocking).
  • Use pipelining for multiple operations.
  • Key convention: {app}:{domain}:{entity}:{id}.
  • Monitor with: redis-cli info memory and redis-cli monitor (dev only).
  • Set maxmemory-policy allkeys-lru for cache-only Redis instances.
用于创建和管理架构决策记录(ADR)。当涉及重大技术选型、模式变更或长期权衡时,按标准模板在docs/specs/下生成文档,涵盖背景、方案对比、成本及安全影响,以记录最终决策及后续行动。
需要在两种或以上非平凡技术方案中进行选择 引入新技术或框架 改变现有重要设计模式 做出具有已知长期影响的权衡
.cursor/skills/rfc-adr/SKILL.md
npx skills add kinncj/Heimdall --skill rfc-adr -g -y
SKILL.md
Frontmatter
{
    "name": "rfc-adr",
    "description": "Author and manage Architecture Decision Records (ADRs) in docs\/specs\/. Use when a significant architectural decision needs to be documented."
}

SKILL: RFC / Architecture Decision Records

ADR Format

Every significant architectural decision gets an ADR. Store in: docs/specs/{feature}/adr.md

# ADR-{N}: {Short Title}

## Status
Proposed | Accepted | Deprecated | Superseded by ADR-{N}

## Context
What is the problem or situation that requires a decision?
What constraints exist?

## Goals
- {Specific, measurable goal}

## Non-goals
- {What this ADR does NOT address}

## Proposal
{Detailed technical proposal. Include code snippets if helpful.}

## Alternatives Considered

### Option A: {Name}
**Description:** {What it is}
**Pros:** {list}
**Cons:** {list}

### Option B: {Name}
**Description:** {What it is}
**Pros:** {list}
**Cons:** {list}

## Trade-offs and Risks
{Analysis of trade-offs. What could go wrong?}

## Impact

### Cost (FinOps)
{Estimated cloud cost impact. Monthly estimate if possible.}

### Operations (SRE)
{New runbook requirements. Alert thresholds. On-call implications.}

### Security
{Changes to attack surface. New threat vectors. Compliance implications.}

### Team
{Skill requirements. Training needed. Hiring implications.}

## Decision
{Final decision and rationale. Who made it and when.}

## Next Steps
- [ ] {Action item with owner}
- [ ] {Action item with owner}

When to Write an ADR

  • Choosing between two or more non-trivial technical approaches.
  • Adopting a new technology or framework.
  • Changing a significant existing pattern.
  • Making a trade-off with known long-term implications.

When NOT to Write an ADR

  • Obvious choices with no real alternatives.
  • Implementation details within an agreed-upon approach.
  • Temporary decisions expected to change soon.
提供独立代码审查,支持计划、代码和测试三种模式。用于捕获遗漏问题并给出批准或修改建议,辅助决策。
Phase 3 计划完成后 复杂多文件实现后 编写测试后运行前 对当前进展感到不确定时
.cursor/skills/rubber-duck/SKILL.md
npx skills add kinncj/Heimdall --skill rubber-duck -g -y
SKILL.md
Frontmatter
{
    "name": "rubber-duck",
    "description": "Invoke the rubber duck reviewer for a second opinion. Use after planning, after complex multi-file implementations, or after writing tests. Produces a focused CRITICAL\/WARN\/INFO list and a APPROVE\/REQUEST_CHANGES verdict."
}

rubber-duck skill

Invoke @rubber-duck to get an independent second opinion on a plan, implementation, or test suite. This is the same pattern as GitHub Copilot CLI's built-in Rubber Duck feature — a different perspective to catch what the primary agent missed.

When to invoke

Checkpoint What to pass What you get back
After Phase 3 (plan complete) plan.md + test-plan.md PLAN REVIEW
After complex multi-file implementation Changed files / diff CODE REVIEW
After tests written, before running Test files TEST REVIEW
Any time you feel uncertain Anything relevant Focused critique

How to call it

@rubber-duck Mode: PLAN REVIEW

Plan: <paste plan.md content or path>
Story: <paste story frontmatter + scenarios>
@rubber-duck Mode: CODE REVIEW

Files changed:
- src/scheduler.ts
- src/jobs/notify.ts
- src/queue/index.ts

<paste diff or file content>
@rubber-duck Mode: TEST REVIEW

Test files:
- tests/unit/scheduler.test.ts
- tests/e2e/notify.spec.ts
- features/scheduler.feature

<paste test content>

Verdict handling

Verdict Orchestrator action
APPROVE Proceed to next phase
REQUEST_CHANGES Send findings to the responsible agent for revision; re-review once

CRITICAL findings always block. WARN findings are surfaced to the human for a decision. INFO findings are logged but do not block.

Tips

  • You do not need to prepare anything special — just pass the relevant content.
  • The rubber duck will not rewrite your code. It only surfaces findings. Acting on them is your job.
  • For Copilot CLI users: the built-in /experimental Rubber Duck activates automatically at the same checkpoints. This skill adds equivalent coverage when using Claude Code or OpenCode.
在提交代码前运行ship-safe安全审计,检查密钥、漏洞和风险模式。需启用环境变量,根据严重程度(高/中/低)报告结果,阻塞高危问题以确保生产安全。
提交PR前 合并功能分支到主分支前 添加新依赖后 修改认证或基础设施配置后
.cursor/skills/ship-safe/SKILL.md
npx skills add kinncj/Heimdall --skill ship-safe -g -y
SKILL.md
Frontmatter
{
    "name": "ship-safe",
    "description": "Run ship-safe security and quality audit on the current project. Executes npx ship-safe audit . and reports findings by severity. Use before shipping any feature or PR."
}

SKILL: Ship-Safe Audit

What It Does

Runs ship-safe — a pre-ship security and quality scanner that checks for secrets, vulnerabilities, and risky patterns before code reaches production.

Usage

npx ship-safe audit .

Run from the project root. No install required (npx fetches it on demand).

Opt-In

Ship-safe is disabled by default. To enable it:

  • CI/CD: set the repository variable ENABLE_SHIP_SAFE=true in GitHub → Settings → Variables
  • Agents / local: set env var ENABLE_SHIP_SAFE=true before invoking /ship-safe

When to Use

Only run if ENABLE_SHIP_SAFE=true is set. When enabled, appropriate moments are:

  • Before opening a PR
  • After adding new dependencies
  • Before merging any feature branch to main
  • After touching auth, secrets handling, or infra config

Output Interpretation

Symbol / keyword Severity Action
✓ PASS / ok / no issues Clean Safe to ship
⚠ WARN / MEDIUM / LOW Advisory Review before shipping
✗ FAIL / ERROR / CRITICAL / HIGH Blocker Must fix before shipping

Agent Instructions

  1. Run npx ship-safe audit . from the repo root.
  2. Parse stdout for CRITICAL/HIGH findings — these are blockers.
  3. For each blocker: report the file, line, and finding description.
  4. For MEDIUM/LOW: report as advisory, do not block.
  5. If all checks pass: confirm "ship-safe: clean" and proceed.
  6. If blockers found: halt the task, report findings to Orchestrator.

Example Integration (architect / pre-ship checklist)

/ship-safe

The skill runs the audit, colors findings by severity, and surfaces blockers before any merge action.

用于在实现前生成Gherkin格式的规范故事文件。遵循状态机流程,输出至docs/stories/,包含元数据、用户故事及验收标准。提供脚本验证文件存在性、Gherkin语法及审批状态,确保需求明确后再进入开发阶段。
需要为新功能编写规范文档 要求将Gherkin故事作为最小化规格说明 执行故事文件有效性验证
.cursor/skills/spec-kit/SKILL.md
npx skills add kinncj/Heimdall --skill spec-kit -g -y
SKILL.md
Frontmatter
{
    "name": "spec-kit",
    "description": "Write a Gherkin story file to docs\/stories\/ for a feature. The story IS the spec. No intermediate PROBLEM\/SPEC\/PLAN\/TASKS artifacts."
}

SKILL: spec-kit

Purpose

Produce a complete Gherkin story file in docs/stories/ before any implementation begins. One invocation = one story file. The story file is the minimum spec — no additional artifacts required.

State Machine

feature description → story draft → human approval → DISCOVER

No step may begin until the previous one completes. Approval = human sets status: approved in the story frontmatter, or confirms verbally.

Story File Location

docs/stories/{epic-slug}-{feature-slug}.md   # with epic context
docs/stories/{feature-slug}.md               # standalone feature

Story File Template

---
id: "{feature-slug}"
title: "{Feature Title}"
epic: "{epic-slug or null}"
priority: "high|medium|low"
ui: false
adr_required: false
phase: discover
labels:
  - "type:feature"
  - "priority:{priority}"
status: draft
---

## Story

**As a** {user role},
**I want** {what they want},
**so that** {business outcome}.

## Acceptance Criteria

```gherkin
@story:{feature-slug} @priority:{priority}
Feature: {Feature Title}

  Scenario: {happy path title}
    Given {precondition}
    When {action}
    Then {outcome}

  Scenario: {failure or edge case title}
    Given {precondition}
    When {action}
    Then {outcome}

Definition of Done

  • All Gherkin scenarios have passing step implementations
  • Unit tests written and passing
  • Code reviewed and approved
  • Documentation updated if user-facing

## Validate a Story File

```bash
validate_story() {
  local file="$1"

  if [ ! -f "$file" ]; then
    echo "[spec-kit] MISSING  $file"
    return 1
  fi

  if ! grep -qE "^\s*(Scenario|Scenario Outline):" "$file"; then
    echo "[spec-kit] FAIL  $file — no Gherkin scenarios found"
    return 1
  fi

  STATUS=$(grep -m1 "^status:" "$file" | awk '{print $2}' | tr -d '"')
  if [ "$STATUS" != "approved" ]; then
    echo "[spec-kit] BLOCKED  $file  status=$STATUS  required=approved"
    return 1
  fi

  echo "[spec-kit] OK  $file  status=approved"
}

# Example
validate_story "docs/stories/user-auth-reset-password.md" || exit 1

Check All Stories Are Approved

FAIL=0
STORIES=$(find docs/stories -name "*.md" ! -name "_template.md" 2>/dev/null || true)

if [ -z "$STORIES" ]; then
  echo "[spec-kit] SKIP  no story files found"
  exit 0
fi

for story in $STORIES; do
  validate_story "$story" || FAIL=1
done

exit $FAIL

Scenario Count Guidelines

Feature complexity Minimum scenarios
Simple CRUD 2 (happy path + not-found/validation failure)
Auth / permissions 3 (success + unauthorized + invalid input)
Multi-step workflow 1 per step + 1 end-to-end
UI component 2 (renders correctly + interaction)

Do not invent scenarios not implied by the feature description. Mark unknown cases as TODO and flag for human review.

Skip Conditions

BRANCH=$(git branch --show-current)
if echo "$BRANCH" | grep -qE '^(spike|chore)/'; then
  echo "[spec-kit] SKIP  branch=$BRANCH — spike/chore exempt"
  exit 0
fi

Failure Modes

Condition Action
No feature description given Ask the human before writing anything
Story has zero Scenario: blocks Refuse to emit. Require at least one scenario.
Story status: rejected Surface rejection reason. Do not advance.
Duplicate story file exists Confirm with human before overwriting.

Logging

[spec-kit] WRITE   docs/stories/user-auth-reset-password.md
[spec-kit] HALT    awaiting human approval
[spec-kit] OK      docs/stories/user-auth-reset-password.md  status=approved
[spec-kit] SKIP    spike/* branch — spec-kit not required
评估新功能上线前的运维就绪状态,涵盖故障模式分析、可观测性指标与日志规范、告警策略及回滚计划。确保具备快速检测、恢复能力,并配套标准化运行手册以保障生产稳定性。
新功能发布前评审 生产环境变更准备
.cursor/skills/sre-review/SKILL.md
npx skills add kinncj/Heimdall --skill sre-review -g -y
SKILL.md
Frontmatter
{
    "name": "sre-review",
    "description": "Evaluate operational readiness: SLOs, alerting, runbooks, and rollback plan before launch. Use when reviewing a feature for production readiness."
}

SKILL: SRE Review

Purpose

Evaluate operational readiness of new features before launch.

Failure Mode Analysis

For each new component, document:

  1. What can fail? (service down, latency spike, data corruption, cascade)
  2. Blast radius? (who is affected? how many users?)
  3. Detection time? (how quickly will alerts fire?)
  4. Recovery time? (how long to restore service?)
  5. Can it be rolled back? (feature flag? migration rollback?)

Observability Requirements

Metrics (Prometheus/CloudWatch/Datadog)

{feature}_requests_total{status="success|error"} counter
{feature}_request_duration_seconds histogram
{feature}_active_connections gauge
{feature}_errors_total{type="validation|timeout|upstream"} counter

Logs (Structured JSON)

{
  "timestamp": "ISO8601",
  "level": "info|warn|error",
  "service": "{service-name}",
  "trace_id": "{distributed-trace-id}",
  "user_id": "{anonymized}",
  "action": "{what happened}",
  "duration_ms": 42,
  "result": "success|error",
  "error": "{message if error}"
}

Traces

  • Instrument all cross-service calls with OpenTelemetry.
  • Trace IDs propagated via W3C Trace Context headers.

Alerts

Alert Condition Severity Action
High error rate error_rate > 1% for 5m P1 Page on-call
Slow responses p99 > 2s for 10m P2 Notify team
Saturation CPU > 80% for 15m P2 Scale out

Runbook Template

docs/runbooks/{feature}-runbook.md

# Runbook: {Feature Name}

## Symptoms
- {Alert name}: {what the user sees}

## Diagnosis
1. Check logs: `kubectl logs -l app={service} --tail=100`
2. Check metrics: {dashboard URL}
3. Check dependencies: {health check commands}

## Mitigation
### Option A: Restart service
```bash
kubectl rollout restart deployment/{service}
kubectl rollout status deployment/{service}

Option B: Feature flag off

# Disable via environment variable
kubectl set env deployment/{service} FEATURE_{NAME}_ENABLED=false

Escalation

  • On-call: {PagerDuty rotation}
  • Escalation: {eng manager}
  • War room: {Slack channel}
维护故事Markdown文件与GitHub Issue的双向一致性。根据所有权模型,文件主导叙事和Gherkin,Issue主导标签、状态及DoD。支持新建同步、正文更新及反向信息写入,确保两者数据对齐。
创建新的故事文件且issue_number为空时 故事文件的Gherkin或验收标准发生变更时
.cursor/skills/story-issue-sync/SKILL.md
npx skills add kinncj/Heimdall --skill story-issue-sync -g -y
SKILL.md
Frontmatter
{
    "name": "story-issue-sync",
    "description": "Maintain bidirectional consistency between story markdown files and GitHub Issues. Use when syncing story status with GitHub."
}

SKILL: story-issue-sync

Purpose

Maintain bidirectional consistency between story files (docs/stories/*.md) and GitHub Issues. The file is authoritative for narrative and Gherkin. The issue is authoritative for labels, status, project board position, and DoD checklist state.

Ownership Model

Attribute Authoritative source Sync direction
Title Story file (frontmatter title) File → Issue
Body / Gherkin Story file File → Issue (on update)
issue_number Issue (assigned at create) Issue → File
issue_url Issue Issue → File
Labels Issue Issue → File (labels frontmatter)
Status / Phase Issue labels Issue → File (labels frontmatter)
DoD checklist Issue body (task list) Issue → File (on review)
Project board position Project v2 Not persisted to file

Inputs / Outputs

Field Source Notes
story_file docs/stories/*.md Path to the story markdown file
issue_number Issue (post-create) Written back to story frontmatter
issue_node_id gh issue create --json id Passed to gh-projects skill for board add
issue_url Issue (post-create) Written back to story frontmatter

Create: File → Issue

When a new story file exists with issue_number: null:

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

# Extract frontmatter fields
TITLE=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.search(r'^title:\s*[\"\'](.*?)[\"\']', text, re.MULTILINE)
print(m.group(1) if m else '')
")

LABELS=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.findall(r'^\s+- [\"\'](.*?)[\"\']\s*$', text, re.MULTILINE)
print(','.join(m))
")

MILESTONE=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.search(r'^milestone:\s*[\"\'](.*?)[\"\']', text, re.MULTILINE)
print(m.group(1) if m else '')
")

# Create the issue
RESULT=$(gh issue create \
  --title "$TITLE" \
  --body-file "$STORY_FILE" \
  --label "$LABELS" \
  --milestone "$MILESTONE" \
  --json number,url,id)

ISSUE_NUMBER=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['number'])")
ISSUE_URL=$(echo "$RESULT"    | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")

# Write back to story frontmatter
python3 - <<EOF
import re
text = open('$STORY_FILE').read()
text = re.sub(r'^issue_number: null', f'issue_number: $ISSUE_NUMBER', text, flags=re.MULTILINE)
text = re.sub(r'^issue_url: null', f'issue_url: "$ISSUE_URL"', text, flags=re.MULTILINE)
open('$STORY_FILE', 'w').write(text)
EOF

echo "[story-sync] CREATED  #$ISSUE_NUMBER  ← $STORY_FILE"

Update: File → Issue (body changed)

When story Gherkin or acceptance criteria are edited:

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

gh issue edit "$ISSUE_NUMBER" \
  --body-file "$STORY_FILE"

echo "[story-sync] BODY_UPDATE  #$ISSUE_NUMBER  ← $STORY_FILE"

Sync: Issue → File (labels changed)

When labels on the issue change (e.g., after PO updates priority or phase advances):

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

# Fetch current labels from issue
CURRENT_LABELS=$(gh issue view "$ISSUE_NUMBER" \
  --json labels \
  --jq '[.labels[].name] | join(",")')

# Rewrite labels block in frontmatter
python3 - <<EOF
import re
text = open('$STORY_FILE').read()
label_lines = '\n'.join(f'  - "{l}"' for l in "$CURRENT_LABELS".split(',') if l)
text = re.sub(
    r'^labels:\n(  - .*\n)+',
    f'labels:\n{label_lines}\n',
    text,
    flags=re.MULTILINE
)
open('$STORY_FILE', 'w').write(text)
EOF

echo "[story-sync] LABELS_SYNC  #$ISSUE_NUMBER  → $STORY_FILE"

Detect Drift (file vs issue)

Run before any update to check whether file and issue are in sync:

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

FILE_TITLE=$(python3 -c "
import re
m = re.search(r'^title:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else '')
")

ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --json title --jq '.title')

if [ "$FILE_TITLE" != "$ISSUE_TITLE" ]; then
  echo "[story-sync] DRIFT  title mismatch: file='$FILE_TITLE'  issue='$ISSUE_TITLE'"
  # File is authoritative for title — update the issue
  gh issue edit "$ISSUE_NUMBER" --title "$FILE_TITLE"
fi

Batch Sync (all stories)

find docs/stories -name "*.md" ! -name "_template.md" | while read -r f; do
  NUMBER=$(python3 -c "
import re
m = re.search(r'^issue_number:\s*(\d+)', open('$f').read(), re.MULTILINE)
print(m.group(1) if m else '')
")
  if [ -z "$NUMBER" ]; then
    echo "[story-sync] NEEDS_CREATE  $f"
  else
    echo "[story-sync] HAS_ISSUE    $f  → #$NUMBER"
  fi
done

Failure Modes

Condition Action
Story has issue_number: null Create the issue. Never skip.
Issue number in file but issue is closed Log warning. Do not reopen automatically — escalate to human.
Title drift detected File wins. Update issue title.
Labels on issue unknown to label set Log: [story-sync] UNKNOWN_LABEL {name}. Do not remove.
Story file missing after issue exists Log warning. Issue is not deleted. Human decides.

Logging

[story-sync] CREATED       #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] BODY_UPDATE   #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] LABELS_SYNC   #42  → docs/stories/user-auth-reset-0001.md
[story-sync] DRIFT         #42  title mismatch — issue updated
[story-sync] SKIP          #42  no changes detected
提供 Stripe API 集成最佳实践,涵盖 Webhook 签名验证、幂等性键使用及订阅处理。包含本地测试命令、TypeScript 代码示例及安全规则,确保支付集成的可靠性与安全性。
需要实现 Stripe Webhook 接收与处理逻辑 创建 Stripe Checkout Session 或处理支付意图 配置 Stripe 本地开发环境进行调试 涉及支付相关的安全合规要求
.cursor/skills/stripe-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill stripe-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "stripe-patterns",
    "description": "Apply Stripe API integration patterns: webhooks, idempotency keys, and subscription handling. Use when integrating Stripe payments."
}

SKILL: Stripe Patterns

Local Testing Setup

# Listen for webhooks and forward to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger checkout.session.completed
stripe trigger customer.subscription.created

# Monitor logs
stripe logs tail

Webhook Handler Pattern

// ALWAYS verify webhook signatures — never skip
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get('stripe-signature')!;
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
  } catch (err) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }

  // Idempotency: check if event already processed
  // Store event.id in database, skip if duplicate

  switch (event.type) {
    case 'payment_intent.succeeded': {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      // Handle success
      break;
    }
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      // Provision access
      break;
    }
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return new Response(JSON.stringify({ received: true }), { status: 200 });
}

Checkout Session Pattern

const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  payment_method_types: ['card'],
  line_items: [{
    price: priceId,
    quantity: 1,
  }],
  success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${baseUrl}/cancel`,
  customer_email: user.email,
  metadata: {
    userId: user.id,  // link back to your user
  },
}, {
  idempotencyKey: `checkout-${user.id}-${priceId}`,  // ALWAYS use idempotency keys
});

Rules

  • ALWAYS verify webhook signatures with constructEvent().
  • ALWAYS use idempotency keys on write operations.
  • NEVER log raw card data or full payment method details.
  • Use restricted API keys with minimum required permissions per service.
  • Test every webhook handler with stripe trigger.
  • Store Stripe customer IDs in your database for lookup.
提供 Supabase 本地开发、SQL 迁移及边缘函数模式。涵盖 RLS 安全策略、类型生成、服务角色规范,确保构建过程中的安全性与最佳实践。
需要创建或配置 Supabase 数据库表结构 编写 Supabase 边缘函数逻辑 配置行级安全策略 (RLS) 执行 Supabase 本地开发工作流
.cursor/skills/supabase-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill supabase-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "supabase-patterns",
    "description": "Apply Supabase schema, RLS policies, and edge function patterns. Use when building on Supabase."
}

SKILL: Supabase Patterns

Local Development Workflow

# Start local Supabase stack
supabase start

# Apply migrations
supabase db push

# Generate TypeScript types
supabase gen types typescript --local > src/types/supabase.ts

# Deploy Edge Functions
supabase functions deploy {function-name}

# Stop
supabase stop

Migration Pattern

-- supabase/migrations/{timestamp}_{description}.sql
-- Up migration (always additive in production)

CREATE TABLE IF NOT EXISTS public.{table_name} (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  created_at timestamptz DEFAULT now() NOT NULL,
  updated_at timestamptz DEFAULT now() NOT NULL
);

-- Enable RLS ALWAYS
ALTER TABLE public.{table_name} ENABLE ROW LEVEL SECURITY;

-- RLS Policies
CREATE POLICY "{table_name}_select_own"
  ON public.{table_name} FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "{table_name}_insert_own"
  ON public.{table_name} FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "{table_name}_update_own"
  ON public.{table_name} FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "{table_name}_delete_own"
  ON public.{table_name} FOR DELETE
  USING (auth.uid() = user_id);

Edge Function Pattern

// supabase/functions/{name}/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req: Request) => {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' }
    })
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    { global: { headers: { Authorization: authHeader } } }
  )

  // Never use service_role key in Edge Functions unless absolutely necessary
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
  }

  // ... handler logic
})

Rules

  • Enable RLS on EVERY table with user data. No exceptions.
  • Never expose service_role key to client code.
  • Always use supabase gen types typescript for type safety.
  • Migrations are append-only (never modify existing migration files).
  • Test with supabase start before deploying.
驱动TDD开发流程,遵循红-绿-重构循环。QA编写失败测试,专家实现最小代码并通过测试,随后重构优化。支持ATDD模式及集成测试容器生命周期管理,确保测试先于实现且质量可靠。
需要实现新功能 执行测试驱动开发任务
.cursor/skills/tdd-workflow/SKILL.md
npx skills add kinncj/Heimdall --skill tdd-workflow -g -y
SKILL.md
Frontmatter
{
    "name": "tdd-workflow",
    "description": "Drive development with a red-green-refactor TDD cycle, ensuring tests are written before implementation. Use when implementing any new functionality."
}

SKILL: TDD Workflow

The RED → GREEN → REFACTOR Cycle

RED Phase (QA Agent)

  1. Read the acceptance criterion or task description.
  2. Write a test that will fail because the implementation doesn't exist.
  3. Run the test. It MUST fail with a meaningful error (not a syntax error).
  4. If the test passes immediately → the test is wrong. Rewrite it.
  5. Report: file path, test name, failure message.

GREEN Phase (Specialist Agent)

  1. Read the failing test file at the provided path.
  2. Implement the MINIMUM code to make ONLY that test pass.
  3. Do not implement anything not required by the test.
  4. Run the test. It must pass.
  5. Run the full unit suite to verify no regressions.
  6. If the test fails after 3 attempts → escalate to Orchestrator.

REFACTOR Phase (Specialist Agent)

  1. Look for: duplication, poor naming, long functions, complex conditionals.
  2. Clean up without changing behavior.
  3. Run the test again. It must still pass.
  4. Commit: git commit -m "refactor: {what was cleaned up}"

ATDD Pattern (Acceptance Test-Driven Development)

  1. Write the E2E/acceptance test from the acceptance criterion (Given/When/Then).
  2. Watch it fail (RED).
  3. Drive out unit tests and implementation to make it pass (GREEN).
  4. Acceptance test goes green last.

Integration Test Container Lifecycle

# Before integration tests
docker compose -f docker-compose.test.yml up -d --wait

# Run integration tests
make test-integration

# After tests
docker compose -f docker-compose.test.yml down -v

Rules

  • Test file is created BEFORE implementation file.
  • Test name format: "should {expected outcome} when {condition}".
  • Never weaken an assertion to make a test pass.
  • Never mock what you can test with a real dependency (use containers).
  • Implementation agent receives FILE PATH, not requirement text.
  • Gate: test must fail before implementation, pass after.
提供Terraform基础设施即代码的最佳实践,涵盖模块化结构、状态管理及变量模式。规范工作流与规则,强调成本标注、验证检查及禁止无指令执行Apply,确保安全高效的云资源管理。
编写或重构Terraform配置 设计基础设施模块结构 管理多环境状态文件 优化Terraform工作流
.cursor/skills/terraform-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill terraform-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "terraform-patterns",
    "description": "Apply Terraform module structure, state management, and variable patterns for infrastructure as code. Use when writing Terraform."
}

SKILL: Terraform Patterns

Module Structure

infra/terraform/
├── modules/
│   ├── networking/
│   ├── database/
│   └── compute/
└── environments/
    ├── dev/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── terraform.tfvars
    ├── staging/
    └── prod/

Module Pattern

# modules/database/main.tf
resource "aws_db_instance" "main" {
  # finops: ~$180/mo (db.t3.medium, 100GB gp3, single AZ)
  identifier     = "${var.environment}-${var.name}-db"
  engine         = "postgres"
  engine_version = "16"
  instance_class = var.instance_class

  allocated_storage     = var.storage_gb
  storage_type          = "gp3"
  storage_encrypted     = true

  username = var.username
  password = var.password

  vpc_security_group_ids = [aws_security_group.db.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  backup_retention_period = var.environment == "prod" ? 7 : 1
  deletion_protection     = var.environment == "prod"

  tags = merge(var.tags, {
    Environment = var.environment
    ManagedBy   = "terraform"
  })
}

Workflow

# Initialize (first time or after provider changes)
terraform init

# Format check (CI)
terraform fmt -check -recursive

# Validate syntax
terraform validate

# Plan (always before apply)
terraform plan -out=plan.tfplan -var-file=environments/dev/terraform.tfvars

# Apply (requires explicit instruction)
terraform apply plan.tfplan

# State inspection
terraform show
terraform state list

Rules

  • ALWAYS run terraform validate before reporting complete.
  • ALWAYS run terraform plan to show what will change.
  • NEVER run terraform apply without explicit instruction.
  • ALWAYS annotate resources with # finops: cost estimate.
  • Use workspaces or separate state files per environment.
  • Enable state locking (S3 + DynamoDB or Terraform Cloud).
  • Tag all resources with environment, managed-by, and cost-center.
基于STRIDE框架对组件和信任边界进行威胁建模,生成包含资产、风险分析及缓解措施的威胁登记册。适用于功能发布前的安全审查,确保识别并缓解潜在安全风险。
新功能安全评审 系统架构变更评估 敏感数据处理流程审查
.cursor/skills/threat-modeling/SKILL.md
npx skills add kinncj/Heimdall --skill threat-modeling -g -y
SKILL.md
Frontmatter
{
    "name": "threat-modeling",
    "description": "Perform STRIDE threat modeling for each component and trust boundary and produce a threat register. Use when reviewing a feature for security."
}

SKILL: Threat Modeling (STRIDE)

Output Location

docs/specs/{feature-slug}/threat-model.md

STRIDE Framework

For each component and trust boundary, evaluate all 6 threat categories:

Letter Threat Question
S Spoofing Can an attacker impersonate a user, service, or system?
T Tampering Can data be modified in transit or at rest without detection?
R Repudiation Can a user deny performing an action?
I Information Disclosure Can sensitive data leak to unauthorized parties?
D Denial of Service Can an attacker prevent legitimate users from accessing the service?
E Elevation of Privilege Can an attacker gain capabilities beyond what is authorized?

Threat Model Template

# Threat Model: {Feature Name}

## Assets
| Asset | Sensitivity | Owner |
|-------|-------------|-------|
| User PII | High | {team} |
| Payment data | Critical | {team} |
| API keys | Critical | {team} |

## Trust Boundaries
- Internet <-> Load Balancer
- Load Balancer <-> Application
- Application <-> Database
- Application <-> Third-party APIs

## STRIDE Analysis

### {Component Name}

**S — Spoofing**
- Risk: {description}
- Likelihood: High/Medium/Low
- Impact: High/Medium/Low
- Mitigation: {control}

**T — Tampering**
...

**R — Repudiation**
...

**I — Information Disclosure**
...

**D — Denial of Service**
...

**E — Elevation of Privilege**
...

## Risk Register
| Threat | Component | Likelihood | Impact | Risk Level | Mitigation | Status |
|--------|-----------|------------|--------|------------|------------|--------|
| SQL Injection | API | High | Critical | Critical | Parameterized queries | Mitigated |

## Mitigations Required Before Launch
- [ ] {Critical mitigation 1}
- [ ] {Critical mitigation 2}

Common Mitigations

  • Authentication: JWT with short expiry, refresh token rotation.
  • Authorization: RBAC, RLS on database.
  • Input validation: Zod/FluentValidation on all inputs.
  • Rate limiting: Per-IP and per-user limits.
  • Encryption: TLS 1.3 in transit, AES-256 at rest.
  • Audit logging: All write operations logged with actor + timestamp.
  • Secrets management: Never in code; use environment variables or vault.
提供Vercel部署、Edge函数及环境变量配置的最佳实践。涵盖vercel.json设置、运行时选择(Edge/Node)、环境同步、构建部署流程及中间件模式,指导高效安全的Vercel应用开发。
需要配置Vercel部署参数 决定使用Edge或Node.js运行时 管理Vercel环境变量 编写Vercel中间件逻辑 执行本地构建与生产部署
.cursor/skills/vercel-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill vercel-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "vercel-patterns",
    "description": "Apply Vercel deployment, edge function, and environment variable patterns. Use when deploying to Vercel."
}

SKILL: Vercel Patterns

vercel.json Configuration

{
  "framework": "nextjs",
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm ci",
  "regions": ["iad1"],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Strict-Transport-Security", "value": "max-age=31536000" }
      ]
    }
  ],
  "rewrites": [],
  "redirects": [
    {
      "source": "/old-path",
      "destination": "/new-path",
      "permanent": true
    }
  ]
}

Runtime Selection

// Edge Runtime (low-latency, streaming, middleware)
export const runtime = 'edge';

// Node.js Runtime (heavy compute, native modules, file system)
export const runtime = 'nodejs';

// ISR (static with revalidation)
export const revalidate = 3600; // seconds

Environment Variables

# Pull env vars for local development
vercel env pull .env.local

# Add env var
vercel env add SECRET_KEY production

# List env vars
vercel env ls

Deployment Workflow

# Build locally first (catch errors before deploy)
vercel build

# Deploy prebuilt (faster, recommended for CI)
vercel deploy --prebuilt

# Deploy to production
vercel deploy --prod --prebuilt

Middleware Pattern

// middleware.ts (runs at edge, before every request)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Auth check, rate limiting, A/B testing, etc.
  const token = request.cookies.get('token');
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Rules

  • Run vercel build locally before deploying to catch errors.
  • Use vercel env pull to sync environment variables locally.
  • Prefer Edge runtime for middleware and simple API routes.
  • Use Node.js runtime for heavy compute or native module requirements.
  • Set security headers for all API routes.
根据品牌简报生成视觉识别系统(配色、排版、间距),输出为JSON供design-tokens使用。支持Web和终端目标,确保WCAG对比度合规,需人工审批后生效。
需要建立品牌视觉风格 输入品牌关键词或描述以生成设计令牌
.cursor/skills/visual-identity/SKILL.md
npx skills add kinncj/Heimdall --skill visual-identity -g -y
SKILL.md
Frontmatter
{
    "name": "visual-identity",
    "description": "Produce a coherent visual identity (palette, typography, spacing, radius) from a brief and serialise to JSON for the design-tokens skill. Use when establishing brand identity."
}

SKILL: visual-identity

Purpose

Produce a coherent visual identity from a brief or set of brand keywords. Outputs are palette, typography pairing, spacing scale, and radius scale — all serialised to JSON files that feed the design-tokens skill. Human approval required before tokens propagate to UI.

Inputs

Field Source Example
brief free-text brand description "modern fintech, trustworthy, minimal"
keywords 3–6 brand keywords ["trust", "clarity", "speed"]
primary_color optional hex override "#2563eb"
existing_tokens path to existing tokens.json docs/design/identity/tokens.json

Outputs

File Location Description
palette.json docs/design/identity/ All color roles with hex + WCAG contrast
typography.json docs/design/identity/ Font families, scale, weights, line-heights
tokens.json docs/design/identity/ W3C DTCG format — canonical token file

Target awareness

When design.target (project.config.yaml, default web) is tui, keep the same JSON outputs but constrain choices to the terminal: colors must be expressible in ANSI-256 (record the nearest 256 index alongside each hex), every fg/bg role pair must clear WCAG 2.2 AA contrast, and typography.json describes terminal text styles (bold, dim/faint, italic, underline, reverse) rather than font families and px sizes. The palette/typography/scale templates below still apply structurally; adapt their values for the terminal.

Palette Structure

docs/design/identity/palette.json:

{
  "brand": {
    "primary":   { "value": "#2563eb", "on": "#ffffff", "contrast_aa": true },
    "secondary": { "value": "#7c3aed", "on": "#ffffff", "contrast_aa": true },
    "accent":    { "value": "#0ea5e9", "on": "#ffffff", "contrast_aa": true }
  },
  "semantic": {
    "success":  { "value": "#16a34a", "on": "#ffffff", "contrast_aa": true },
    "warning":  { "value": "#d97706", "on": "#000000", "contrast_aa": true },
    "error":    { "value": "#dc2626", "on": "#ffffff", "contrast_aa": true },
    "info":     { "value": "#0284c7", "on": "#ffffff", "contrast_aa": true }
  },
  "neutral": {
    "50":  "#f8fafc",
    "100": "#f1f5f9",
    "200": "#e2e8f0",
    "300": "#cbd5e1",
    "400": "#94a3b8",
    "500": "#64748b",
    "600": "#475569",
    "700": "#334155",
    "800": "#1e293b",
    "900": "#0f172a"
  },
  "surface": {
    "background": "#ffffff",
    "foreground": "#0f172a",
    "muted":      "#f1f5f9",
    "border":     "#e2e8f0"
  }
}

WCAG Contrast Check

All palette entries with on values must pass WCAG 2.2 AA (4.5:1 for normal text, 3:1 for large text):

def relative_luminance(hex_color):
    r, g, b = (int(hex_color[i:i+2], 16) / 255 for i in (1, 3, 5))
    def linearize(c):
        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    r, g, b = linearize(r), linearize(g), linearize(b)
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def contrast_ratio(fg, bg):
    l1 = relative_luminance(fg)
    l2 = relative_luminance(bg)
    lighter, darker = max(l1, l2), min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

# AA normal text requires >= 4.5
# AA large text requires >= 3.0
ratio = contrast_ratio("#2563eb", "#ffffff")
assert ratio >= 4.5, f"FAIL: contrast {ratio:.2f} < 4.5 for #2563eb on #ffffff"

If any pair fails AA, adjust the shade until it passes. Never ship a palette with failing contrast.

Typography Structure

docs/design/identity/typography.json:

{
  "families": {
    "sans":  "Inter, system-ui, -apple-system, sans-serif",
    "mono":  "JetBrains Mono, 'Fira Code', monospace",
    "serif": null
  },
  "scale": {
    "xs":   { "size": "0.75rem",  "line": "1rem" },
    "sm":   { "size": "0.875rem", "line": "1.25rem" },
    "base": { "size": "1rem",     "line": "1.5rem" },
    "lg":   { "size": "1.125rem", "line": "1.75rem" },
    "xl":   { "size": "1.25rem",  "line": "1.75rem" },
    "2xl":  { "size": "1.5rem",   "line": "2rem" },
    "3xl":  { "size": "1.875rem", "line": "2.25rem" },
    "4xl":  { "size": "2.25rem",  "line": "2.5rem" }
  },
  "weights": {
    "regular": 400,
    "medium":  500,
    "semibold": 600,
    "bold":    700
  }
}

Spacing + Radius Scales

Written directly into tokens.json (see design-tokens skill for full schema):

{
  "spacing": {
    "1":  "0.25rem",
    "2":  "0.5rem",
    "3":  "0.75rem",
    "4":  "1rem",
    "6":  "1.5rem",
    "8":  "2rem",
    "12": "3rem",
    "16": "4rem"
  },
  "radius": {
    "none": "0",
    "sm":   "0.125rem",
    "md":   "0.375rem",
    "lg":   "0.5rem",
    "xl":   "0.75rem",
    "full": "9999px"
  }
}

Write palette.json

mkdir -p docs/design/identity
# Write palette.json from the template above, populated with chosen values
# Then validate all contrast ratios before writing
python3 scripts/validate-palette.py docs/design/identity/palette.json

Approval Gate

Mark docs/design/identity/palette.json approved before design-tokens runs:

Add to the top of palette.json:

{ "_meta": { "status": "draft", "approved_by": null, "approved_at": null }, ... }

Check in design-tokens skill:

STATUS=$(python3 -c "import json; print(json.load(open('docs/design/identity/palette.json'))['_meta']['status'])")
[ "$STATUS" = "approved" ] || { echo "BLOCKED: palette not approved"; exit 1; }

Failure Modes

Condition Action
Contrast ratio fails AA Adjust shade. Re-check. Never skip.
existing_tokens provided Merge: only fill missing roles; do not overwrite approved values.
No brief or keywords provided Generate a neutral/professional default palette. Log DEFAULT_PALETTE.
palette.json exists and is approved Do not overwrite. Log SKIP — approved identity exists.

Logging

[visual-identity] PALETTE    docs/design/identity/palette.json  contrast=all-pass
[visual-identity] TYPOGRAPHY docs/design/identity/typography.json
[visual-identity] SKIP       docs/design/identity/palette.json  (approved — locked)
[visual-identity] CONTRAST_FAIL  #2563eb on #ffffff  ratio=3.8  required=4.5
根据用户故事文件生成低保真线框图,支持ASCII、SVG或HTML格式。适用于UI故事设计阶段,需人工审批后方可进入后续mockup工作,确保输出确定性。
需要为UI故事创建低保真线框图时 从用户故事文件生成界面布局原型时
.cursor/skills/wireframe/SKILL.md
npx skills add kinncj/Heimdall --skill wireframe -g -y
SKILL.md
Frontmatter
{
    "name": "wireframe",
    "description": "Generate low-fidelity wireframes (ASCII, SVG, or HTML) from user story files. Use when creating wireframes for UI stories."
}

SKILL: wireframe

Purpose

Generate low-fidelity wireframes from user story files. Output is deterministic — given the same story and layout hints, the same wireframe structure is produced. Three output formats: ASCII (default), SVG, HTML. Human approval is required before the wireframe feeds downstream mockup work.

Inputs

Field Source Example
story_file path to story markdown docs/stories/auth-reset-0001.md
ui_components derived from story Gherkin form, button, error message
stack project.config.yaml react-mantine

Outputs

The required files depend on design.target in project.config.yaml (default web) — see Target awareness:

File Location Targets
<story-id>.wireframe.md docs/design/wireframes/ web + tui — ASCII layout + approval metadata
<story-id>.wireframe.excalidraw docs/design/wireframes/ web + tui — editable Excalidraw diagram
<story-id>.wireframe.html docs/design/wireframes/ web only — browser-previewable static wireframe

A wireframe stage that produces fewer than the files required for the active target is incomplete. Do not PAUSE or mark DONE without them.

Target awareness

design.target decides which files are required:

  • web<id>.wireframe.md (ASCII) + .html (preview) + .excalidraw.
  • tui<id>.wireframe.md (ASCII/box-drawing) + .excalidraw. No .html.

Use box-drawing primitives for tui layouts, label panes/overlays/status bar, and include the keybinding legend and focus order inline in the .md. The Excalidraw generator below applies to both targets; the HTML generator is web only.

ASCII Wireframe Primitives

Use these consistently across all wireframes:

┌─────────────────────────────────┐   ← container / card
│  [Label]  [Input Field      ]   │   ← label + text input
│  [Button: Primary Action    ]   │   ← primary button
│  [Button: Secondary]            │   ← secondary button
│  ○ Option A  ○ Option B         │   ← radio group
│  ☐ Checkbox label               │   ← checkbox
│  ▼ Dropdown / Select            │   ← select / combobox
│  ──────────────────────         │   ← divider
│  ⚠ Error message text           │   ← validation error
│  ✓ Success confirmation         │   ← success state
└─────────────────────────────────┘

[Nav: Logo | Item 1 | Item 2 | CTA]  ← navigation bar
[ Sidebar  ][     Main Content    ]  ← two-column layout
[  Col 1  ][  Col 2  ][  Col 3  ]   ← three-column grid
[         Full-width Banner         ]← hero / header band

ASCII Wireframe — Example (Password Reset)

┌──────────────────────────────────────┐
│            Reset Password            │
│                                      │
│  Email                               │
│  [                              ]    │
│                                      │
│  ⚠ No account found for this email  │  ← error state
│                                      │
│  [Button: Send Reset Link       ]    │
│                                      │
│  ← Back to Login                     │
└──────────────────────────────────────┘

Generate Wireframe Files

For each story, produce all three output files. Run these steps:

STORY_FILE="docs/stories/auth-reset-password-20250416143000-0001.md"
STORY_ID=$(python3 -c "
import re
m = re.search(r'^id:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
mkdir -p docs/design/wireframes

Step 1 — Write ${STORY_ID}.wireframe.md (ASCII layout + approval metadata):

---
story_id: "{story_id}"
story_file: "{story_file}"
status: draft          # draft | approved | rejected
approved_by: null
approved_at: null
---

## Wireframe: {story title}

### Default state
{ASCII wireframe}

### Error state
{ASCII wireframe — validation error}

### Success state
{ASCII wireframe — confirmation}

### Interaction Notes

- Tab order: {list of focusable elements in tab sequence}
- Primary action: {describe}
- Error handling: {describe visible error states}

### Approval

- [ ] Approved by product owner
- [ ] Approved by UX lead (if applicable)

Step 2 — Write ${STORY_ID}.wireframe.html (see HTML template below)

Step 3 — Write ${STORY_ID}.wireframe.excalidraw (see Excalidraw template below)

After writing the files required for the active target, verify they exist (web: md, html, excalidraw; tui: md, excalidraw):

ls docs/design/wireframes/${STORY_ID}.wireframe.md
ls docs/design/wireframes/${STORY_ID}.wireframe.excalidraw
# web target also:
ls docs/design/wireframes/${STORY_ID}.wireframe.html 2>/dev/null || true

If any required file is missing, produce it before continuing.

HTML Wireframe (web target only)

For web targets, always generate the HTML wireframe. Skip this section entirely for tui. Use multiple <section> blocks for multi-state wireframes (default, loading, error, success, empty).

cat > "docs/design/wireframes/${STORY_ID}.wireframe.html" <<'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Wireframe: {story title}</title>
  <style>
    * { box-sizing: border-box; font-family: monospace; }
    body { background: #e9ecef; padding: 2rem; }
    h1 { font-size: 1rem; color: #495057; margin-bottom: 1.5rem; }
    .states { display: flex; flex-wrap: wrap; gap: 1.5rem; }
    .state { background: #f8f9fa; border: 1px solid #adb5bd; border-radius: 4px; padding: 0; min-width: 360px; }
    .state-label { background: #343a40; color: #fff; font-size: .75rem; padding: .25rem .75rem; border-radius: 4px 4px 0 0; }
    .frame { padding: 1.5rem; }
    .screen-title { font-weight: bold; font-size: 1.1rem; margin-bottom: 1rem; border-bottom: 1px solid #dee2e6; padding-bottom: .5rem; }
    label { display: block; font-size: .8rem; color: #495057; margin-bottom: .2rem; margin-top: .75rem; }
    .input { border: 1px solid #868e96; padding: .4rem .6rem; width: 100%; background: #fff; }
    .btn { border: none; padding: .5rem 1rem; cursor: default; margin-top: .75rem; width: 100%; font-weight: bold; }
    .btn-primary { background: #343a40; color: #fff; }
    .btn-secondary { background: transparent; border: 1px solid #343a40; color: #343a40; }
    .error { color: #c0392b; font-size: .8rem; margin-top: .25rem; }
    .success { color: #2d6a4f; font-size: .8rem; margin-top: .25rem; }
    .link { color: #1971c2; font-size: .85rem; margin-top: .75rem; display: block; }
    .nav { display: flex; gap: 1rem; background: #343a40; color: #fff; padding: .6rem 1rem; font-size: .85rem; margin-bottom: .75rem; }
    .badge { background: #868e96; color: #fff; font-size: .7rem; padding: .1rem .4rem; border-radius: 3px; }
    .divider { border: none; border-top: 1px solid #dee2e6; margin: .75rem 0; }
    .tab-order { font-size: .7rem; color: #868e96; margin-top: 1.5rem; border-top: 1px dashed #dee2e6; padding-top: .5rem; }
  </style>
</head>
<body>
  <h1>Wireframe: {story title} — {story_id}</h1>
  <div class="states">

    <div class="state">
      <div class="state-label">Default state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <!-- Add form fields, buttons, content blocks here -->
        <label>Field label</label>
        <input class="input" type="text" placeholder="placeholder" disabled />
        <div class="btn btn-primary">Primary Action</div>
        <a class="link" href="#">Secondary link</a>
        <div class="tab-order">Tab order: Field → Primary Action → Secondary link</div>
      </div>
    </div>

    <div class="state">
      <div class="state-label">Error state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <label>Field label</label>
        <input class="input" type="text" placeholder="placeholder" disabled style="border-color:#c0392b" />
        <div class="error">⚠ Error message describing the problem</div>
        <div class="btn btn-primary">Primary Action</div>
        <div class="tab-order">Tab order: Field → Primary Action</div>
      </div>
    </div>

    <div class="state">
      <div class="state-label">Success state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <div class="success">✓ Success confirmation message</div>
        <div class="btn btn-secondary">Back / Next step</div>
      </div>
    </div>

  </div>
</body>
</html>
HTMLEOF

Extend with real field names, content, and states from the story Gherkin. One <div class="state"> block per Gherkin scenario.

Excalidraw Wireframe (always required)

Always generate the Excalidraw file — every story, every run. Excalidraw is the canonical editable wireframe format reviewers annotate.

Write the file as valid JSON to docs/design/wireframes/${STORY_ID}.wireframe.excalidraw. Each UI element is one entry in the elements array. Use the element templates below, copy and adapt:

python3 - <<'PYEOF'
import json, pathlib, os

story_id = os.environ.get("STORY_ID", "unknown")
out = pathlib.Path(f"docs/design/wireframes/{story_id}.wireframe.excalidraw")
out.parent.mkdir(parents=True, exist_ok=True)

# ── Element helpers ──────────────────────────────────────────────────────────
def rect(id, x, y, w, h, label="", bg="transparent", stroke="#343a40", bold=False):
    els = [{
        "id": id, "type": "rectangle", "x": x, "y": y, "width": w, "height": h,
        "angle": 0, "strokeColor": stroke, "backgroundColor": bg,
        "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid",
        "roughness": 1, "opacity": 100, "groupIds": [], "roundness": {"type": 3},
        "version": 1, "versionNonce": 1, "isDeleted": False,
        "boundElements": None, "updated": 1, "link": None, "locked": False,
    }]
    if label:
        els.append(text(id + "_lbl", x + w/2, y + h/2, label, bold=bold, anchor="center"))
    return els

def text(id, x, y, content, bold=False, anchor="left", color="#343a40"):
    return {
        "id": id, "type": "text", "x": x, "y": y,
        "width": len(content) * 8, "height": 20,
        "angle": 0, "strokeColor": color, "backgroundColor": "transparent",
        "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid",
        "roughness": 1, "opacity": 100, "groupIds": [], "roundness": None,
        "version": 1, "versionNonce": 1, "isDeleted": False,
        "boundElements": None, "updated": 1, "link": None, "locked": False,
        "text": content, "fontSize": 16,
        "fontFamily": 3,  # monospace
        "textAlign": anchor, "verticalAlign": "middle",
        "baseline": 14, "containerId": None, "originalText": content,
        "lineHeight": 1.25,
        "fontWeight": "bold" if bold else "normal",
    }

def input_field(id, x, y, w, label_text):
    return (
        [text(id + "_lbl", x, y - 18, label_text)] +
        rect(id, x, y, w, 32, stroke="#868e96")
    )

def button_primary(id, x, y, w, label_text):
    return rect(id, x, y, w, 36, label=label_text, bg="#343a40", stroke="#343a40", bold=True)

def button_secondary(id, x, y, w, label_text):
    return rect(id, x, y, w, 36, label=label_text, bg="transparent", stroke="#343a40")

def section_label(id, x, y, content):
    return [text(id, x, y, f"[ {content} ]", color="#868e96")]

# ── Build elements ────────────────────────────────────────────────────────────
elements = []

# Outer frame
elements += rect("frame", 50, 30, 700, 600, stroke="#343a40")

# Screen title
elements.append(text("title", 70, 50, "{Screen Title}", bold=True))

# --- Default state ---
elements += section_label("s_default", 70, 90, "Default state")
elements += input_field("field1", 70, 130, 560, "Field label")
elements += button_primary("btn_primary", 70, 190, 560, "Primary Action")
elements.append(text("link1", 70, 238, "← Secondary link / back", color="#1971c2"))

# --- Error state ---
elements += section_label("s_error", 70, 280, "Error state")
elements += input_field("field1_err", 70, 320, 560, "Field label")
elements += rect("err_border", 70, 320, 560, 32, stroke="#c0392b")
elements.append(text("err_msg", 70, 360, "⚠ Error message text", color="#c0392b"))
elements += button_primary("btn_primary_err", 70, 390, 560, "Primary Action")

# --- Success state ---
elements += section_label("s_success", 70, 450, "Success state")
elements.append(text("success_msg", 70, 490, "✓ Success confirmation", color="#2d6a4f"))
elements += button_secondary("btn_back", 70, 520, 260, "Back / Next step")

doc = {
    "type": "excalidraw",
    "version": 2,
    "source": "MAPLE wireframe-architect",
    "elements": elements,
    "appState": {"viewBackgroundColor": "#f8f9fa", "gridSize": None},
    "files": {},
}
out.write_text(json.dumps(doc, indent=2) + "\n")
print(f"[wireframe] wrote {out}")
PYEOF

Adapt element positions and labels to match the actual story screens. Add more rect/text/input_field/button_primary calls per Gherkin scenario. Do not leave placeholder text ({Screen Title}) in the final file.

Approval Gate

All three files must exist and the .md must be status: approved before mockup or ui-mockup-builder proceeds:

python3 - <<'EOF'
import re, sys, pathlib
sid = open(".claude/state/maple.json").read()  # or pass STORY_ID
# Check required artifacts exist (html is web-only)
cfg = open("project.config.yaml").read() if pathlib.Path("project.config.yaml").exists() else ""
tm = re.search(r'^\s*target:\s*(\w+)', cfg, re.MULTILINE)
target = tm.group(1) if tm else "web"
exts = ["md", "excalidraw"] if target == "tui" else ["md", "html", "excalidraw"]
for ext in exts:
    p = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.{ext}")
    if not p.exists():
        print(f"BLOCKED: missing {p}")
        sys.exit(1)
# Check approval status in .md
md = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.md").read_text()
m = re.search(r'^status:\s*(\w+)', md, re.MULTILINE)
status = m.group(1) if m else 'draft'
if status != 'approved':
    print(f"BLOCKED: wireframe {sid} not approved (status={status})")
    sys.exit(1)
print("approved — all three artifacts present")
EOF

Failure Modes

Condition Action
Story has no Gherkin Generate skeleton wireframe with placeholder states. Log NO_GHERKIN — skeleton only.
docs/design/wireframes/ missing Create it.
Wireframe .md exists and is approved Do not overwrite. Log SKIP — approved wireframe exists.
Wireframe .md exists and is draft Overwrite only if story Gherkin has changed.
.html or .excalidraw missing despite .md existing Generate the missing file(s) immediately.

Logging

[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.md
[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.html
[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.excalidraw
[wireframe] SKIP     docs/design/wireframes/auth-reset-0001.wireframe.md  (approved — locked)
[wireframe] BLOCKED  auth-reset-0001  status=draft — needs approval before mockup
[wireframe] BLOCKED  auth-reset-0001  missing .html — generating now
针对生成UI执行WCAG 2.2 AA无障碍审计。当故事含ui:true或收到审计请求时触发。使用axe-core或pa11y检测,将结果发布为PR评论并拦截违规合并。支持Web和TUI目标,自动检测工具并生成标准化报告。
故事包含 ui:true 用户明确要求进行无障碍审计
.opencode/skills/a11y-audit/SKILL.md
npx skills add kinncj/Heimdall --skill a11y-audit -g -y
SKILL.md
Frontmatter
{
    "name": "a11y-audit",
    "description": "Run WCAG 2.2 Level AA accessibility audits against generated UI. Use when a story has ui:true or when asked to audit accessibility."
}

SKILL: a11y-audit

Purpose

Run WCAG 2.2 Level AA accessibility audits against generated UI. Uses axe-core (via @axe-core/cli) or pa11y as available. Posts findings as PR comments. Blocks merge on AA violations. Required for every story with ui: true.

Target awareness

When design.target (project.config.yaml, default web) is tui, skip the browser tooling (axe/pa11y) and the preview URL. Instead evaluate the terminal a11y checklist from docs/design/design-targets.md and write the findings to docs/design/mockups/<id>.a11y.json using the SAME object shape the web path produces — an object with violations[], where each violation has id, impact (critical|serious|moderate|minor), description, help, and nodes[] ({target:[...], failureSummary}). The gate (scripts/sdlc/a11y-gate.sh) reads only violations[].impact, so matching this shape keeps it unchanged. Map checklist failures to impacts: keyboard-reachable→critical, focus-visible→serious, color-contrast→serious, color-only-signaling→serious, no-color-support→moderate, min-width-resize→moderate. The WCAG criteria, tool-detection, axe/pa11y, and PR-comment sections below apply to web targets.

WCAG 2.2 AA — Minimum Requirements

Criterion Level What to check
1.1.1 Non-text Content A All <img> have alt. Icons used as UI controls have labels.
1.3.1 Info and Relationships A Semantic HTML: headings, lists, tables, landmarks used correctly.
1.4.3 Contrast (minimum) AA Normal text ≥ 4.5:1. Large text ≥ 3:1.
1.4.11 Non-text Contrast AA UI components and focus indicators ≥ 3:1 against background.
2.1.1 Keyboard A All interactive elements reachable and operable via keyboard.
2.4.3 Focus Order A Tab order is logical and follows visual layout.
2.4.7 Focus Visible AA Focus indicator visible on all interactive elements.
3.3.1 Error Identification A Errors identified in text, not color alone.
3.3.2 Labels or Instructions A All inputs have labels.
4.1.2 Name, Role, Value A All UI components have accessible name, role, and state.

Tool Detection

detect_tool() {
  if command -v axe &>/dev/null; then
    echo "axe"
  elif command -v pa11y &>/dev/null; then
    echo "pa11y"
  elif npx --yes @axe-core/cli --version &>/dev/null 2>&1; then
    echo "axe-npx"
  else
    echo "none"
  fi
}
TOOL=$(detect_tool)

Run with axe-core CLI

URL="${1:-http://localhost:3000}"  # preview URL or Storybook story URL
STORY_ID="${2:-unknown}"
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

mkdir -p docs/design/mockups

axe "$URL" \
  --stdout \
  --tags wcag2a,wcag2aa,wcag21aa,wcag22aa \
  > "$REPORT"

VIOLATIONS=$(python3 -c "import json,sys; d=json.load(open('$REPORT')); print(len(d.get('violations',[])))")
echo "[a11y-audit] axe  $URL  violations=$VIOLATIONS"

Run with pa11y

URL="${1:-http://localhost:3000}"
STORY_ID="${2:-unknown}"
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

pa11y "$URL" \
  --standard WCAG2AA \
  --reporter json \
  > "$REPORT" 2>&1

ISSUES=$(python3 -c "import json,sys; d=json.load(open('$REPORT')); print(len(d) if isinstance(d,list) else 0)")
echo "[a11y-audit] pa11y  $URL  issues=$ISSUES"

Parse Results and Classify

import json, sys

report_path = sys.argv[1]
data = json.load(open(report_path))

# axe format
violations = data.get("violations", [])
passes     = data.get("passes", [])

critical = [v for v in violations if v["impact"] in ("critical", "serious")]
moderate = [v for v in violations if v["impact"] == "moderate"]
minor    = [v for v in violations if v["impact"] == "minor"]

print(f"CRITICAL/SERIOUS: {len(critical)}")
print(f"MODERATE:         {len(moderate)}")
print(f"MINOR:            {len(minor)}")
print(f"PASSES:           {len(passes)}")

# Merge gate: block on critical + serious
if critical:
    print("\nMERGE BLOCKED — resolve the following before merging:")
    for v in critical:
        print(f"  [{v['impact'].upper()}] {v['id']}: {v['description']}")
        for node in v.get("nodes", [])[:2]:
            print(f"    → {node.get('target', ['?'])[0]}: {node.get('failureSummary','')[:120]}")
    sys.exit(1)

Post Findings as PR Comment

STORY_ID="auth-reset-0001"
PR_NUMBER=$(gh pr list --head "$(git branch --show-current)" --json number --jq '.[0].number')
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

# Build comment body
COMMENT=$(python3 - <<'EOF'
import json, sys

data = json.load(open(sys.argv[1]))
violations = data.get("violations", [])

if not violations:
    print("## ✅ A11y Audit Passed\n\nNo WCAG 2.2 AA violations found.")
    sys.exit(0)

lines = [f"## ⚠️ A11y Audit: {len(violations)} violation(s) found\n"]
for v in violations[:10]:
    impact = v.get("impact","").upper()
    lines.append(f"### [{impact}] {v['id']}")
    lines.append(f"_{v['description']}_\n")
    for node in v.get("nodes",[])[:2]:
        selector = ', '.join(node.get("target",[]))
        lines.append(f"- `{selector}`")
        lines.append(f"  {node.get('failureSummary','')[:200]}")
    lines.append("")

if len(violations) > 10:
    lines.append(f"_...and {len(violations)-10} more. See full report in `docs/design/mockups/`._")

print('\n'.join(lines))
EOF
"$REPORT")

gh pr comment "$PR_NUMBER" --body "$COMMENT"
echo "[a11y-audit] PR_COMMENT  #$PR_NUMBER  violations=$(echo "$COMMENT" | grep -c '\[CRITICAL\]\|\[SERIOUS\]' || echo 0)"

Merge Gate Check

Called from scripts/sdlc/a11y-gate.sh (added in Phase VII):

STORY_FILE="$1"
UI=$(python3 -c "
import re
m = re.search(r'^ui:\s*(true|false)', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'false')
")

if [ "$UI" != "true" ]; then
  echo "[a11y-audit] SKIP  ui:false story — no audit required"
  exit 0
fi

STORY_ID=$(python3 -c "
import re
m = re.search(r'^id:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
REPORT="docs/design/mockups/${STORY_ID}.a11y.json"

if [ ! -f "$REPORT" ]; then
  echo "[a11y-audit] FAIL  no audit report found for $STORY_ID — run audit before merging"
  exit 1
fi

python3 scripts/sdlc/parse-a11y-report.py "$REPORT"

Manual Audit Checklist (no tool available)

When TOOL=none, perform a structured manual check:

## Manual A11y Checklist — {story_id}

### Perceivable
- [ ] All images have meaningful `alt` text (or `alt=""` for decorative)
- [ ] Color is not the sole means of conveying information
- [ ] Normal text contrast ≥ 4.5:1 (check with browser DevTools)
- [ ] Large text contrast ≥ 3:1

### Operable
- [ ] Tab through the entire form/component with keyboard only
- [ ] All actions reachable without mouse
- [ ] No keyboard trap
- [ ] Focus indicator visible on every interactive element

### Understandable
- [ ] Form inputs have visible labels (not just placeholders)
- [ ] Errors described in text, not color alone
- [ ] Error messages are specific and actionable

### Robust
- [ ] Correct semantic elements: `<button>`, `<input>`, `<label>`
- [ ] ARIA attributes only where native HTML is insufficient
- [ ] Works with browser zoom at 200%

Failure Modes

Condition Action
No tool available Fall back to manual checklist. Log TOOL=none — manual audit required.
Preview URL not reachable Log URL_UNREACHABLE. Do not skip audit — escalate.
ui: false story Skip audit. Log SKIP — ui:false.
Critical violations found Post PR comment. Exit non-zero. Block merge.
Report already exists and is current Skip re-run. Log SKIP — current report exists.

Logging

[a11y-audit] RUN      http://localhost:6006/... story=auth-reset-0001  tool=axe
[a11y-audit] RESULT   violations=0  passes=24  WCAG2AA=PASS
[a11y-audit] RESULT   violations=3  critical=1  WCAG2AA=FAIL
[a11y-audit] PR_COMMENT  #42  violations=1
[a11y-audit] SKIP     ui:false story — audit not required
[a11y-audit] BLOCKED  merge blocked — 1 critical violation unresolved
根据设计令牌和原型自动生成完整的UI组件文件树,包括实现、Storybook故事、单元测试及Gherkin规范。支持React-Mantine等框架,生成含TODO占位符的可运行骨架代码。
需要创建新的UI组件 初始化组件脚手架
.opencode/skills/component-scaffold/SKILL.md
npx skills add kinncj/Heimdall --skill component-scaffold -g -y
SKILL.md
Frontmatter
{
    "name": "component-scaffold",
    "description": "Generate a complete component file tree wired to design tokens with tests and Gherkin spec. Use when scaffolding a new UI component."
}

SKILL: component-scaffold

Purpose

Generate a complete component file tree wired to design tokens. Each component gets an implementation file, Storybook story, unit test, and Gherkin spec file. Skeletons are runnable with TODO stubs — not empty files. Stack is auto-detected from project.config.yaml.

Inputs

Field Source Example
component_name PascalCase PasswordResetForm
story_id story frontmatter id auth-reset-0001
tokens_file docs/design/identity/tokens.json required
stack project.config.yaml react-mantine | react-tailwind | html
mockup_file approved mockup docs/design/mockups/auth-reset-0001.mockup.tsx

Output Tree

For PasswordResetForm with stack react-mantine:

app/
└── components/
    └── PasswordResetForm/
        ├── index.tsx              ← component implementation
        ├── PasswordResetForm.stories.tsx  ← Storybook story
        ├── PasswordResetForm.test.tsx     ← unit tests (Vitest / Jest)
        └── PasswordResetForm.spec.ts      ← Gherkin step binding

Generate Component Index (react-mantine)

app/components/{ComponentName}/index.tsx:

// {ComponentName} — generated from story {story_id}
// Tokens: docs/design/identity/mantine.theme.ts
// TODO: implement from mockup docs/design/mockups/{story_id}.mockup.tsx

import { Stack, TextInput, Button } from '@mantine/core';
import { useState } from 'react';

export interface {ComponentName}Props {
  // TODO: define props from story acceptance criteria
  onSubmit?: (data: Record<string, unknown>) => void;
}

export function {ComponentName}({ onSubmit }: {ComponentName}Props) {
  // TODO: implement
  return (
    <Stack>
      {/* TODO: build from approved mockup */}
    </Stack>
  );
}

export default {ComponentName};

Generate Storybook Story

app/components/{ComponentName}/{ComponentName}.stories.tsx:

import type { Meta, StoryObj } from '@storybook/react';
import { {ComponentName} } from '.';

const meta: Meta<typeof {ComponentName}> = {
  title: 'Components/{ComponentName}',
  component: {ComponentName},
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof {ComponentName}>;

export const Default: Story = {
  args: {},
};

export const WithError: Story = {
  args: {
    // TODO: add error props
  },
};

Generate Unit Test

app/components/{ComponentName}/{ComponentName}.test.tsx:

import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { {ComponentName} } from '.';

describe('{ComponentName}', () => {
  it('renders without crashing', () => {
    render(<{ComponentName} />);
    // TODO: assert presence of key elements from wireframe
    expect(document.body).toBeTruthy();
  });

  it('calls onSubmit when form is submitted', () => {
    // TODO: implement interaction test
  });

  it('shows validation error when input is invalid', () => {
    // TODO: implement error state test
  });
});

Generate Gherkin Step Binding

app/components/{ComponentName}/{ComponentName}.spec.ts:

// Gherkin step bindings for story {story_id}
// Feature file: tests/features/{epic}/{story_slug}.feature
import { Given, When, Then } from '@cucumber/cucumber';

// TODO: copy matching steps from cucumber-automation output
// Example:
// Given('the user is on the {string} page', async (page: string) => {
//   throw new Error('Pending');
// });

Python Stack Alternative

For stack=python (e.g., Django / HTMX):

app/
└── components/
    └── password_reset_form/
        ├── __init__.py
        ├── template.html
        ├── test_password_reset_form.py
        └── password_reset_form_steps.py

Scaffold Script

COMPONENT="PasswordResetForm"
STORY_ID="auth-reset-0001"
STACK="react-mantine"
DIR="app/components/$COMPONENT"

mkdir -p "$DIR"

# Check for existing files — never overwrite
for f in "index.tsx" "${COMPONENT}.stories.tsx" "${COMPONENT}.test.tsx" "${COMPONENT}.spec.ts"; do
  if [ -f "$DIR/$f" ]; then
    echo "[component-scaffold] SKIP  $DIR/$f  (already exists)"
  else
    echo "[component-scaffold] CREATE  $DIR/$f"
    # write the appropriate template (see above)
  fi
done

Token Wiring

After scaffold, inject token import into index.tsx:

# Verify mantine.theme.ts exists
[ -f "docs/design/identity/mantine.theme.ts" ] || \
  echo "[component-scaffold] WARN  mantine.theme.ts missing — run design-tokens skill"

Failure Modes

Condition Action
ComponentName not PascalCase Warn and convert: password-reset-formPasswordResetForm.
app/components/ does not exist Create it. Log the creation.
File already exists Skip with SKIP log — never overwrite existing implementations.
tokens.json missing Scaffold without token import. Add // TODO: wire to design tokens comment.
Stack unknown Default to react-mantine. Log warning.

Logging

[component-scaffold] CREATE  app/components/PasswordResetForm/index.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.stories.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.test.tsx
[component-scaffold] CREATE  app/components/PasswordResetForm/PasswordResetForm.spec.ts
[component-scaffold] SKIP    app/components/PasswordResetForm/index.tsx  (already exists)
[component-scaffold] WARN    mantine.theme.ts missing
从故事Markdown文件提取Gherkin场景生成可运行的.feature文件,并自动生成对应测试框架的Step Definition存根。支持TypeScript、JavaScript、Python和Java,保持文档与测试套件同步。
需要将故事文档同步为自动化测试用例 需要为新场景生成步骤定义代码存根
.opencode/skills/cucumber-automation/SKILL.md
npx skills add kinncj/Heimdall --skill cucumber-automation -g -y
SKILL.md
Frontmatter
{
    "name": "cucumber-automation",
    "description": "Extract Gherkin scenarios from story markdown files into runnable .feature files and generate step definition stubs. Use when syncing stories to test suites."
}

SKILL: cucumber-automation

Purpose

Extract Gherkin scenarios from story markdown files into runnable .feature files, and generate step definition stubs for the project's test stack. Keeps tests/features/ in sync with docs/stories/ as the single source of Gherkin truth.

Supported Stacks

Stack Step file extension Framework
TypeScript / Node.js .steps.ts @cucumber/cucumber
JavaScript / Node.js .steps.js @cucumber/cucumber
Python _steps.py behave
Java Steps.java Cucumber-JVM

Detect the stack from the repo root:

if [ -f "package.json" ] && grep -q "@cucumber/cucumber" package.json 2>/dev/null; then
  STACK="typescript"
elif [ -f "requirements.txt" ] && grep -q "behave" requirements.txt 2>/dev/null; then
  STACK="python"
elif [ -f "pom.xml" ] || [ -f "build.gradle" ]; then
  STACK="java"
else
  STACK="unknown"
fi

Extract Gherkin from a Story File

Story files contain Gherkin in fenced code blocks tagged with gherkin:

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"
EPIC="user-auth"

# Extract content of ```gherkin ... ``` blocks
python3 - <<'EOF'
import re, sys

story = open(sys.argv[1]).read()
blocks = re.findall(r'```gherkin\n(.*?)```', story, re.DOTALL)
if not blocks:
    print("NO_GHERKIN", file=sys.stderr)
    sys.exit(1)
print('\n\n'.join(blocks))
EOF "$STORY_FILE"

Write the Feature File

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"
FEATURE_DIR="tests/features"
EPIC="user-auth"

mkdir -p "$FEATURE_DIR/$EPIC"
FEATURE_FILE="$FEATURE_DIR/$EPIC/reset-password.feature"

# Extract and write (idempotent — overwrites if Gherkin changed)
python3 - <<'EOF'
import re, sys, os

story_path, feature_path = sys.argv[1], sys.argv[2]
story = open(story_path).read()
blocks = re.findall(r'```gherkin\n(.*?)```', story, re.DOTALL)
if not blocks:
    print(f"[cucumber] NO_GHERKIN in {story_path}", file=sys.stderr)
    sys.exit(1)

os.makedirs(os.path.dirname(feature_path), exist_ok=True)
with open(feature_path, 'w') as f:
    f.write('\n\n'.join(b.rstrip() for b in blocks))
    f.write('\n')

print(f"[cucumber] WRITE  {feature_path}  scenarios={len(re.findall(r'^  Scenario', ''.join(blocks), re.MULTILINE))}")
EOF "$STORY_FILE" "$FEATURE_FILE"

Generate Step Definition Stubs (TypeScript)

FEATURE_FILE="tests/features/user-auth/reset-password.feature"
STEPS_DIR="tests/step-definitions/user-auth"
mkdir -p "$STEPS_DIR"

python3 - <<'EOF'
import re, sys, os

feature_path = sys.argv[1]
steps_dir = sys.argv[2]
feature = open(feature_path).read()

# Collect unique step texts
steps = re.findall(r'^\s+(Given|When|Then|And|But) (.+)$', feature, re.MULTILINE)
seen = set()
unique = []
for keyword, text in steps:
    norm = re.sub(r'"[^"]+"', '"{string}"', text)
    norm = re.sub(r'\b\d+\b', '{int}', norm)
    if norm not in seen:
        seen.add(norm)
        unique.append((keyword if keyword not in ('And','But') else 'Given', norm, text))

# Build TypeScript file
feature_name = os.path.basename(feature_path).replace('.feature','')
out_path = os.path.join(steps_dir, f"{feature_name}.steps.ts")

if os.path.exists(out_path):
    print(f"[cucumber] SKIP  {out_path}  (already exists)")
    sys.exit(0)

lines = [
    "import { Given, When, Then } from '@cucumber/cucumber';",
    "",
]
for keyword, pattern, original in unique:
    fn = keyword.lower()
    # build parameter list from placeholders
    params = []
    if '{string}' in pattern:
        params += [f"arg{i}: string" for i in range(pattern.count('{string}'))]
    if '{int}' in pattern:
        params += [f"n{i}: number" for i in range(pattern.count('{int}'))]
    param_str = ', '.join(params)
    escaped = pattern.replace("'", "\\'")
    lines += [
        f"// {original}",
        f"{fn}('{escaped}', async ({param_str}) => {{",
        f"  // TODO: implement",
        f"  throw new Error('Pending: {escaped}');",
        f"}});",
        "",
    ]

with open(out_path, 'w') as f:
    f.write('\n'.join(lines))

print(f"[cucumber] STUBS  {out_path}  steps={len(unique)}")
EOF "$FEATURE_FILE" "$STEPS_DIR"

Generate Step Definition Stubs (Python / behave)

FEATURE_FILE="tests/features/user_auth/reset_password.feature"
STEPS_DIR="tests/features/user_auth"
mkdir -p "$STEPS_DIR"

python3 - <<'EOF'
import re, sys, os

feature_path = sys.argv[1]
steps_dir = sys.argv[2]
feature = open(feature_path).read()

steps = re.findall(r'^\s+(Given|When|Then|And|But) (.+)$', feature, re.MULTILINE)
seen = set()
unique = []
for keyword, text in steps:
    norm = re.sub(r'"[^"]+"', '"{text}"', text)
    norm = re.sub(r'\b\d+\b', '{n:d}', norm)
    if norm not in seen:
        seen.add(norm)
        unique.append((keyword if keyword not in ('And','But') else 'given', norm, text))

feature_name = os.path.basename(feature_path).replace('.feature','')
out_path = os.path.join(steps_dir, f"{feature_name}_steps.py")

if os.path.exists(out_path):
    print(f"[cucumber] SKIP  {out_path}  (already exists)")
    sys.exit(0)

lines = ["from behave import given, when, then", ""]
for keyword, pattern, original in unique:
    fn = keyword.lower()
    escaped = pattern.replace('"', '\\"')
    lines += [
        f"# {original}",
        f'@{fn}(u"{escaped}")',
        f"def step_impl(context):",
        f'    raise NotImplementedError(u"STEP: {fn} {escaped}")',
        "",
    ]

with open(out_path, 'w') as f:
    f.write('\n'.join(lines))

print(f"[cucumber] STUBS  {out_path}  steps={len(unique)}")
EOF "$FEATURE_FILE" "$STEPS_DIR"

Batch Sync (all stories → all feature files)

find docs/stories -name "*.md" ! -name "_template.md" | while read -r story; do
  EPIC=$(python3 -c "
import re
m = re.search(r'^epic:\s*[\"\'](.*?)[\"\']', open('$story').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
  SLUG=$(basename "$story" | sed 's/-[0-9]\{14\}-[0-9]\{4\}\.md$//')
  FEATURE_FILE="tests/features/$EPIC/$SLUG.feature"
  # extract-and-write step (see above) ...
  echo "[cucumber] SYNC  $story → $FEATURE_FILE"
done

Playwright + behave Integration

When the stack includes Playwright (requirements.txt contains playwright or behave), step definitions must follow these patterns. Read the QA agent's BDD section for the full antipatterns list.

environment.py template

import json
import threading
from playwright.sync_api import sync_playwright

BASE_URL = "http://localhost:3000"
DEFAULT_GEO = {"latitude": 40.7128, "longitude": -74.0060}

MOCK_FORECAST = {
    "daily": {
        "time": ["2025-01-01","2025-01-02","2025-01-03",
                 "2025-01-04","2025-01-05","2025-01-06","2025-01-07"],
        "temperature_2m_max": [22.1, 18.3, 15.0, 20.5, 25.2, 19.8, 17.4],
        "temperature_2m_min": [12.0, 10.5,  9.3, 13.2, 16.1, 11.4, 10.9],
        "weathercode":        [0,    3,     61,   0,    1,    80,   71 ],
        "precipitation_probability_max": [5, 30, 85, 10, 20, 70, 60],
        "windspeed_10m_max":  [12.0, 20.5, 35.2, 8.4, 15.6, 28.9, 22.3],
        "relative_humidity_2m_max": [55, 72, 90, 48, 62, 85, 78],
    }
}

def before_all(context):
    context.playwright = sync_playwright().start()
    context.browser = context.playwright.chromium.launch()

def after_all(context):
    context.browser.close()
    context.playwright.stop()

def before_scenario(context, scenario):
    context.browser_context = context.browser.new_context(
        permissions=["geolocation"],
        geolocation=DEFAULT_GEO,
    )
    context.page = context.browser_context.new_page()
    context._forecast_event = None

def after_scenario(context, scenario):
    context.page.close()
    context.browser_context.close()

# ── Helpers ────────────────────────────────────────────────────────────────────

def setup_forecast_route(page, error=False, delay_event=None):
    """Network-level Open-Meteo intercept. NEVER overrides window.fetch."""
    def handle(route):
        if delay_event is not None:
            delay_event.wait(timeout=30)
        if error:
            route.fulfill(status=500, content_type="application/json",
                          body='{"error":"service unavailable"}')
        else:
            route.fulfill(status=200, content_type="application/json",
                          body=json.dumps(MOCK_FORECAST))
    page.route("**/api.open-meteo.com/**", handle)

def navigate_and_wait(page):
    page.goto(BASE_URL, wait_until="domcontentloaded")
    page.wait_for_selector(".forecast-card", timeout=10_000)

Browser capability scenarios

# Geolocation granted — default, handled by before_scenario

# Geolocation denied — create context without the permission
context.browser_context = context.browser.new_context()  # no geolocation permission

# Geolocation unsupported — NO Playwright native API; add_init_script is acceptable HERE ONLY
context.page.add_init_script(
    "Object.defineProperty(navigator,'geolocation',{value:undefined,configurable:true});"
)

# Geolocation timeout — NO Playwright native API; add_init_script acceptable HERE ONLY
context.page.add_init_script(
    "navigator.geolocation.getCurrentPosition = function(){};"  # never calls back
)

Loading state (route with threading.Event)

# Setup step — route stays blocked until event is set
event = threading.Event()
setup_forecast_route(context.page, delay_event=event)
context._forecast_event = event

# Assert spinner visible while blocked
spinner = context.page.locator('[role="status"]')
spinner.wait_for(state="visible", timeout=3000)

# Unblock route → cards should appear
context._forecast_event.set()
context.page.wait_for_selector(".forecast-card", timeout=5000)

API error state

setup_forecast_route(context.page, error=True)
# route returns HTTP 500 — app must handle gracefully

Antipatterns checklist (rubber duck will flag these)

Antipattern Why it's wrong
add_init_script("window.fetch = ...") Bypasses all app network code; test passes regardless of implementation
page.evaluate("window._appResolve()") Reaches into app internals; not observable behaviour
add_init_script(GEO_SUCCESS_SCRIPT) for geolocation Playwright's context API exists — use it
page.evaluate("window.__mock = ...") Same as window.fetch override — leaks test state into app
Assertions checking internal state (data-* set by test, not app) Test the rendered output, not the test's own injected data

Failure Modes

Condition Action
Story file has no gherkin fenced block Log NO_GHERKIN. Skip. Do not create empty feature file.
Feature file already exists with manual edits Compare checksums. If changed: log MANUAL_EDIT — skip overwrite. Never overwrite manual edits.
Step file already exists Skip generation. Log SKIP. Agents implement stubs; generator does not regenerate.
Stack is unknown Log warning. Write .feature file only; skip step stubs.
Gherkin parse error (malformed block) Log the offending line. Skip the file. Do not attempt partial extraction.

Logging

[cucumber] WRITE   tests/features/user-auth/reset-password.feature  scenarios=3
[cucumber] STUBS   tests/step-definitions/user-auth/reset-password.steps.ts  steps=7
[cucumber] SKIP    tests/step-definitions/user-auth/login.steps.ts  (already exists)
[cucumber] NO_GHERKIN  docs/stories/auth-spike-20250416143000-0002.md  (spike — expected)
读取和写入W3C DTCG格式的design tokens,并生成CSS、Tailwind和Mantine输出。用于修改或生成设计令牌,tokens.json为唯一数据源。
需要修改设计令牌时 需要生成CSS自定义属性、Tailwind配置或Mantine主题对象时
.opencode/skills/design-tokens/SKILL.md
npx skills add kinncj/Heimdall --skill design-tokens -g -y
SKILL.md
Frontmatter
{
    "name": "design-tokens",
    "description": "Read and write design tokens in W3C DTCG format (tokens.json) and emit CSS, Tailwind, and Mantine outputs. Use when modifying or generating design tokens."
}

SKILL: design-tokens

Purpose

Read and write design tokens in W3C Design Token Community Group (DTCG) format (tokens.json). Emit framework-specific outputs: CSS custom properties, Tailwind config, and Mantine theme object. The tokens.json in docs/design/identity/ is the canonical source of truth; all framework outputs are derived and regenerated — never manually edited.

Token File: docs/design/identity/tokens.json

W3C DTCG format. Each token is an object with $value and $type:

{
  "$schema": "https://design-tokens.org/schema/v1.0.0",
  "_meta": { "status": "draft", "version": "1.0.0" },

  "color": {
    "brand": {
      "primary":   { "$value": "#2563eb", "$type": "color" },
      "secondary": { "$value": "#7c3aed", "$type": "color" },
      "accent":    { "$value": "#0ea5e9", "$type": "color" }
    },
    "semantic": {
      "success": { "$value": "#16a34a", "$type": "color" },
      "warning": { "$value": "#d97706", "$type": "color" },
      "error":   { "$value": "#dc2626", "$type": "color" },
      "info":    { "$value": "#0284c7", "$type": "color" }
    },
    "neutral": {
      "50":  { "$value": "#f8fafc", "$type": "color" },
      "500": { "$value": "#64748b", "$type": "color" },
      "900": { "$value": "#0f172a", "$type": "color" }
    },
    "surface": {
      "background": { "$value": "#ffffff", "$type": "color" },
      "foreground": { "$value": "#0f172a", "$type": "color" },
      "muted":      { "$value": "#f1f5f9", "$type": "color" },
      "border":     { "$value": "#e2e8f0", "$type": "color" }
    }
  },

  "typography": {
    "fontFamily": {
      "sans": { "$value": "Inter, system-ui, sans-serif", "$type": "fontFamily" },
      "mono": { "$value": "JetBrains Mono, monospace",   "$type": "fontFamily" }
    },
    "fontSize": {
      "sm":   { "$value": "0.875rem", "$type": "dimension" },
      "base": { "$value": "1rem",     "$type": "dimension" },
      "lg":   { "$value": "1.125rem", "$type": "dimension" },
      "xl":   { "$value": "1.25rem",  "$type": "dimension" },
      "2xl":  { "$value": "1.5rem",   "$type": "dimension" },
      "4xl":  { "$value": "2.25rem",  "$type": "dimension" }
    }
  },

  "spacing": {
    "1": { "$value": "0.25rem", "$type": "dimension" },
    "2": { "$value": "0.5rem",  "$type": "dimension" },
    "4": { "$value": "1rem",    "$type": "dimension" },
    "8": { "$value": "2rem",    "$type": "dimension" }
  },

  "radius": {
    "sm":   { "$value": "0.125rem", "$type": "dimension" },
    "md":   { "$value": "0.375rem", "$type": "dimension" },
    "lg":   { "$value": "0.5rem",   "$type": "dimension" },
    "full": { "$value": "9999px",   "$type": "dimension" }
  },

  "shadow": {
    "sm": { "$value": "0 1px 2px 0 rgb(0 0 0 / 0.05)", "$type": "shadow" },
    "md": { "$value": "0 4px 6px -1px rgb(0 0 0 / 0.1)", "$type": "shadow" },
    "lg": { "$value": "0 10px 15px -3px rgb(0 0 0 / 0.1)", "$type": "shadow" }
  }
}

Emit: CSS Custom Properties

Output to docs/design/identity/tokens.css:

import json, re

def flatten(obj, prefix=""):
    result = {}
    for k, v in obj.items():
        if k.startswith("$") or k.startswith("_"):
            continue
        key = f"{prefix}-{k}" if prefix else k
        if isinstance(v, dict) and "$value" in v:
            result[key] = v["$value"]
        elif isinstance(v, dict):
            result.update(flatten(v, key))
    return result

tokens = json.load(open("docs/design/identity/tokens.json"))
flat = flatten(tokens)

lines = [":root {"]
for name, value in sorted(flat.items()):
    css_var = "--" + name.replace(".", "-")
    lines.append(f"  {css_var}: {value};")
lines.append("}")

with open("docs/design/identity/tokens.css", "w") as f:
    f.write("\n".join(lines) + "\n")

print(f"[design-tokens] CSS  docs/design/identity/tokens.css  vars={len(flat)}")

Emit: Tailwind Config

Output to docs/design/identity/tailwind.tokens.js:

import json

tokens = json.load(open("docs/design/identity/tokens.json"))

def extract_colors():
    result = {}
    for group, values in tokens.get("color", {}).items():
        result[group] = {}
        for name, token in values.items():
            if "$value" in token:
                result[group][name] = token["$value"]
    return result

colors = extract_colors()
font_family = {
    k: v["$value"]
    for k, v in tokens.get("typography", {}).get("fontFamily", {}).items()
}

config = f"""/** @type {{import('tailwindcss').Config}} */
// AUTO-GENERATED — edit docs/design/identity/tokens.json, not this file
module.exports = {{
  theme: {{
    extend: {{
      colors: {json.dumps(colors, indent(6))},
      fontFamily: {json.dumps(font_family, indent(6))},
    }},
  }},
}};
"""

with open("docs/design/identity/tailwind.tokens.js", "w") as f:
    f.write(config)

print("[design-tokens] TAILWIND  docs/design/identity/tailwind.tokens.js")

Emit: Mantine Theme

Output to docs/design/identity/mantine.theme.ts:

import json

tokens = json.load(open("docs/design/identity/tokens.json"))

primary = tokens["color"]["brand"]["primary"]["$value"]
secondary = tokens["color"]["brand"]["secondary"]["$value"]

theme = f"""// AUTO-GENERATED — edit docs/design/identity/tokens.json, not this file
import {{ createTheme }} from '@mantine/core';

export const theme = createTheme({{
  primaryColor: 'brand',
  colors: {{
    brand: [
      '{tokens["color"]["neutral"]["50"]["$value"]}',
      '{tokens["color"]["neutral"]["100"]["$value"] if "100" in tokens["color"]["neutral"] else "#f1f5f9"}',
      '{tokens["color"]["neutral"]["200"]["$value"] if "200" in tokens["color"]["neutral"] else "#e2e8f0"}',
      '{tokens["color"]["neutral"]["300"]["$value"] if "300" in tokens["color"]["neutral"] else "#cbd5e1"}',
      '{tokens["color"]["neutral"]["400"]["$value"] if "400" in tokens["color"]["neutral"] else "#94a3b8"}',
      '{tokens["color"]["neutral"]["500"]["$value"]}',
      '{primary}',
      '{tokens["color"]["neutral"]["700"]["$value"] if "700" in tokens["color"]["neutral"] else "#334155"}',
      '{tokens["color"]["neutral"]["800"]["$value"] if "800" in tokens["color"]["neutral"] else "#1e293b"}',
      '{tokens["color"]["neutral"]["900"]["$value"]}',
    ],
  }},
  fontFamily: '{tokens["typography"]["fontFamily"]["sans"]["$value"]}',
  fontFamilyMonospace: '{tokens["typography"]["fontFamily"]["mono"]["$value"]}',
}});
"""

with open("docs/design/identity/mantine.theme.ts", "w") as f:
    f.write(theme)

print("[design-tokens] MANTINE  docs/design/identity/mantine.theme.ts")

Emit: Terminal Theme (tui target)

When design.target is tui, emit docs/design/identity/terminal-theme.json from tokens.json instead of the CSS/Tailwind/Mantine outputs. Map each color role to an ANSI-256 index (and the hex) plus a lipgloss style name, and record the foreground/background pairs with their contrast ratios so the a11y auditor can flag any pair under WCAG 2.2 AA. Shape:

{
  "roles": {
    "primary":    { "fg": "#7aa2f7", "ansi256": 111, "lipgloss": "primary" },
    "muted":      { "fg": "#565f89", "ansi256": 60,  "lipgloss": "muted" },
    "accent":     { "fg": "#bb9af7", "ansi256": 141, "lipgloss": "accent" },
    "error":      { "fg": "#f7768e", "ansi256": 204, "lipgloss": "error" },
    "success":    { "fg": "#9ece6a", "ansi256": 149, "lipgloss": "success" },
    "background": { "bg": "#1a1b26", "ansi256": 234 },
    "foreground": { "fg": "#c0caf5", "ansi256": 189 }
  },
  "pairs": [
    { "role": "foreground", "on": "background", "contrast": 12.6 },
    { "role": "muted",      "on": "background", "contrast": 3.4 }
  ]
}

Run All Emitters

python3 - <<'EOF'
# Run all three emitters in sequence
# (inline the three scripts above, or import from scripts/emit-tokens.py)
EOF

Update Tokens (read-write)

To update a single token value:

import json, sys

tokens = json.load(open("docs/design/identity/tokens.json"))
# Set tokens["color"]["brand"]["primary"]["$value"] = "#1d4ed8"
# Write back, then re-run all emitters
json.dump(tokens, open("docs/design/identity/tokens.json", "w"), indent=2)

Always re-emit all framework outputs after any token change.

Failure Modes

Condition Action
tokens.json malformed Log parse error with line number. Do not emit partial output.
Missing required token key Log MISSING_TOKEN: {path}. Emit with placeholder TODO value.
Emitted file differs from committed Acceptable — these are always regenerated.
_meta.status != approved Log warning. Emit anyway (dev workflow). Gate is in visual-identity approval, not here.

Logging

[design-tokens] READ    docs/design/identity/tokens.json  tokens=47
[design-tokens] CSS     docs/design/identity/tokens.css   vars=47
[design-tokens] TAILWIND docs/design/identity/tailwind.tokens.js
[design-tokens] MANTINE  docs/design/identity/mantine.theme.ts
[design-tokens] MISSING_TOKEN  color.neutral.100  — using placeholder
提供Docker和Docker Compose最佳实践,涵盖多阶段构建、健康检查配置及.dockerignore规则。指导编写安全高效的容器化应用,强调非root运行、缓存优化及镜像扫描验证。
编写或审查Dockerfile时 配置Docker Compose服务依赖与健康检查时
.opencode/skills/docker-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill docker-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "docker-patterns",
    "description": "Apply Docker and Docker Compose best practices for containerising services. Use when writing or reviewing Dockerfiles and compose configs."
}

SKILL: Docker Patterns

Multi-Stage Dockerfile Pattern

# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app

FROM base AS deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

FROM base AS dev-deps
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

FROM base AS build
COPY --from=dev-deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs22-debian12 AS production
WORKDIR /app
USER 1001
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["dist/main.js"]

docker-compose Health Check Pattern

services:
  app:
    build:
      context: .
      target: production
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://postgres:pass@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      timeout: 5s
      retries: 10

.dockerignore

node_modules
.git
.github
dist
build
coverage
.next
*.log
.env*
!.env.example
README.md
docs
tests

Key Rules

  • ALWAYS use multi-stage builds for production images.
  • ALWAYS run as non-root user (USER 1001 or named user).
  • ALWAYS add health checks to all services.
  • NEVER copy .env files into images.
  • Use --mount=type=cache for package manager caches.
  • Use distroless or alpine base images.
  • Use docker compose up -d --wait to wait for health checks.

Verification

# Build test
docker build --target production -t test-image .

# Security scan
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image test-image

# Start and verify health
docker compose up -d --wait
docker compose ps
用于识别和标注架构决策的云成本影响。在审查基础设施变更或设计新服务时,提供成本驱动因素分类、Terraform注释格式及月度估算模板,帮助评估扩展性和优化支出。
审查基础设施变更 设计新服务 评估架构扩展性成本
.opencode/skills/finops-review/SKILL.md
npx skills add kinncj/Heimdall --skill finops-review -g -y
SKILL.md
Frontmatter
{
    "name": "finops-review",
    "description": "Identify and annotate cloud cost implications of architectural decisions. Use when reviewing infrastructure changes or designing new services."
}

SKILL: FinOps Review

Purpose

Identify and annotate the cloud cost implications of architectural decisions.

Cost Driver Categories

Compute

  • Lambda/Functions: invocation count x duration x memory
  • EC2/VMs: instance type x hours + data transfer
  • Containers: CPU/memory reservation x hours
  • Edge functions: request count x execution time

Storage

  • Object storage (S3/GCS/Blob): GB stored + requests + egress
  • Database: instance size + storage + IOPS + backups
  • Cache: node size x hours

Networking

  • Egress: GB transferred out (most expensive)
  • CDN: requests + GB served
  • Load balancer: hours + LCU

Third-party APIs

  • Stripe: 2.9% + $0.30 per transaction
  • Supabase: row reads/writes, storage, Edge Function invocations
  • Vercel: bandwidth, function invocations, seats

Annotation Format (Terraform)

resource "aws_db_instance" "main" {
  # finops: ~$180/mo (db.t3.medium, 100GB gp3, 1 AZ)
  # finops: scale-trigger: >70% CPU or >80% storage
  instance_class = "db.t3.medium"
  ...
}

Architecture Review Questions

  1. What scales with user count? What scales with data volume?
  2. Are there N+1 API calls that will cost $$ at scale?
  3. Is egress minimized? (Cache aggressively, colocate services)
  4. Are reserved instances/savings plans applicable?
  5. What is the cost at 10x current load?

Monthly Estimate Template

## Cost Estimate: {Feature}

| Service | Config | Est. Monthly |
|---------|--------|-------------|
| RDS PostgreSQL | db.t3.medium, 100GB | $180 |
| ElastiCache Redis | cache.t3.micro | $25 |
| ECS Fargate | 2 tasks, 0.5 vCPU, 1GB | $30 |
| ALB | 1 ALB, ~1M requests | $20 |
| **Total** | | **~$255/mo** |

Scales linearly with: [traffic volume / data volume]
管理GitHub Issue全生命周期,支持创建、查看、编辑标签与指派、评论及关闭。通过gh CLI自动化关联故事阶段状态,实现从需求到QA闭环的无干预Issue流转。
需要为故事创建新的GitHub Issue 更新Issue的状态、标签或指派人员 在Issue中添加阶段进展或TDD状态评论 查询特定阶段的开放Issue列表
.opencode/skills/gh-issues/SKILL.md
npx skills add kinncj/Heimdall --skill gh-issues -g -y
SKILL.md
Frontmatter
{
    "name": "gh-issues",
    "description": "Create, read, update, and link GitHub Issues as story artifacts throughout the story lifecycle. Use when managing issues for stories."
}

SKILL: gh-issues

Purpose

Create, read, update, and link GitHub Issues as first-class story artifacts. Agents use this skill to manage issue lifecycle — from PO story creation through QA closure — without human intervention.

Inputs

Field Source Example
title story frontmatter "User can reset password"
body_file story file path docs/stories/auth-reset-0001.md
labels story frontmatter + phase "type:feature,priority:high,phase:discover"
milestone project.config.yaml "v1.0"
assignee orchestrator context "@me"
issue_number previously created issue 42

Outputs

Field Description
issue_number integer, use for all subsequent operations
issue_url full GitHub URL for logging
issue_node_id GraphQL node ID, needed for Projects v2 card add

Create an Issue

# Capture issue number and URL
RESULT=$(gh issue create \
  --title "{title}" \
  --body-file "{body_file}" \
  --label "{labels}" \
  --milestone "{milestone}" \
  --json number,url,id)

ISSUE_NUMBER=$(echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['number'])")
ISSUE_URL=$(echo "$RESULT"    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['url'])")
ISSUE_NODE=$(echo "$RESULT"   | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])")

View an Issue

gh issue view {issue_number} --json number,title,state,labels,body

Edit Labels and Assignee

# Add label, remove old phase label
gh issue edit {issue_number} \
  --add-label "phase:architect" \
  --remove-label "phase:discover"

# Assign to current actor
gh issue edit {issue_number} --add-assignee "@me"

Add a Comment

gh issue comment {issue_number} \
  --body "{message}"

Comment conventions by role:

Event Template
Phase start "Phase {N} {PHASE_NAME}: starting. Artifact target: {path}"
Phase complete "Phase {N} {PHASE_NAME}: complete. Artifacts: {path}"
TDD red "RED: Failing test at {test_path} — {failure_message}"
TDD green "GREEN: {test_count} tests passing."
Blocked "BLOCKED: {agent} failed 3x on {task}. Human intervention required."

Close an Issue

# Close with final validation summary
gh issue close {issue_number} \
  --comment "All acceptance criteria passing. Validation: {summary}"

List Issues

# All open issues for current phase
gh issue list --label "phase:implement" --state open --json number,title,labels

# Blocked issues
gh issue list --label "blocked" --state open

Link Issues (parent / blocks)

GitHub Issues has no native parent field — use body references and labels:

# In the child issue body, add:
# "Closes #{parent_number}" → auto-closes parent when child closes
# "Part of #{epic_issue_number}"

# For blocking relationships, comment on the blocked issue:
gh issue comment {blocked_number} \
  --body "Blocked by #{blocker_number} — {reason}"
gh issue edit {blocked_number} --add-label "blocked"

Failure Modes

Condition Action
gh not authenticated Stop. Output: gh auth login required. Do not retry.
Issue already exists with same title Check with gh issue list --search "{title}" --state all. Skip create if found; return existing number.
Label does not exist Create label first via gh label create. See gh-labels-milestones skill.
Milestone not found Create milestone first: gh api repos/{owner}/{repo}/milestones -f title="{name}".
Rate limit hit Wait 60 seconds, retry once. If second attempt fails, stop and log.

Logging

Always log after every gh issue mutation:

[gh-issues] CREATE  #42  "User can reset password"  labels=[type:feature,priority:high]
[gh-issues] COMMENT #42  phase:implement start
[gh-issues] CLOSE   #42  all criteria passing

Format: [gh-issues] {ACTION} #{number} {description}

用于初始化或同步 GitHub 仓库的标签和里程碑,确保状态幂等。通过脚本自动创建缺失项或更新颜色描述,但不删除现有内容,适用于项目启动和 CI 流程。
新项目初始化配置 CI/CD 流水线中同步标签状态 需要统一团队标签规范时
.opencode/skills/gh-labels-milestones/SKILL.md
npx skills add kinncj/Heimdall --skill gh-labels-milestones -g -y
SKILL.md
Frontmatter
{
    "name": "gh-labels-milestones",
    "description": "Bootstrap and sync GitHub labels and milestones idempotently. Use when setting up a new repository or syncing label definitions."
}

SKILL: gh-labels-milestones

Purpose

Bootstrap and sync GitHub labels and milestones idempotently. Labels and milestones are created if absent; existing ones are updated if color or description differs. Never delete — only add or update.

Label Bootstrap

Use this at project start (maple labels) and in CI to guarantee label state.

# Idempotent label create-or-update
upsert_label() {
  local name="$1" color="$2" description="$3"
  if gh label list --json name --jq '.[].name' | grep -qx "$name"; then
    gh label edit "$name" --color "$color" --description "$description"
  else
    gh label create "$name" --color "$color" --description "$description"
  fi
}

Label Groups

Phase Labels (pipeline position)

upsert_label "phase:discover"   "0075ca" "Phase 1: Discovery"
upsert_label "phase:architect"  "0075ca" "Phase 2: Architecture"
upsert_label "phase:plan"       "0075ca" "Phase 3: Planning"
upsert_label "phase:infra"      "0075ca" "Phase 4: Infrastructure"
upsert_label "phase:implement"  "0075ca" "Phase 5: Implementation"
upsert_label "phase:validate"   "0075ca" "Phase 6: Validation"
upsert_label "phase:document"   "0075ca" "Phase 7: Documentation"
upsert_label "phase:done"       "0075ca" "Phase 8: Complete"

Type Labels (work classification)

upsert_label "type:feature"     "0052cc" "New capability"
upsert_label "type:bug"         "d73a4a" "Defect"
upsert_label "type:spike"       "e4e669" "Research / time-boxed exploration"
upsert_label "type:chore"       "ededed" "Non-functional maintenance"
upsert_label "type:refactor"    "ededed" "Code restructuring, no behavior change"
upsert_label "type:docs"        "0075ca" "Documentation only"

Priority Labels (MoSCoW)

upsert_label "priority:critical" "b60205" "Must ship — blocks launch"
upsert_label "priority:high"     "e11d48" "Must have"
upsert_label "priority:medium"   "f97316" "Should have"
upsert_label "priority:low"      "84cc16" "Could have"
upsert_label "priority:wontfix"  "ffffff" "Won't have this cycle"

Spec / Story Kit Labels

upsert_label "spec:problem"     "7057ff" "Problem statement written"
upsert_label "spec:approved"    "7057ff" "Spec approved by PO"
upsert_label "spec:in-review"   "7057ff" "Spec under review"

Design Labels

upsert_label "design:pending"      "fbca04" "Awaiting design"
upsert_label "design:in-progress"  "fbca04" "Design work active"
upsert_label "design:approved"     "fbca04" "Design approved"
upsert_label "design:a11y-passed"  "fbca04" "Accessibility review passed"

ADR Labels

upsert_label "adr:required"    "5319e7" "ADR must be written before implementation"
upsert_label "adr:in-progress" "5319e7" "ADR being authored"
upsert_label "adr:complete"    "5319e7" "ADR accepted"
upsert_label "adr:rejected"    "5319e7" "ADR rejected — see comments"

UI / Accessibility Labels

upsert_label "ui:required"     "fef2c0" "Has UI surface — design review required"
upsert_label "ui:in-progress"  "fef2c0" "UI implementation active"
upsert_label "ui:complete"     "fef2c0" "UI complete and reviewed"

Status Labels

upsert_label "in-progress" "0052cc" "Work started"
upsert_label "blocked"     "b60205" "Blocked — needs human"
upsert_label "validated"   "0e8a16" "All tests pass"
upsert_label "tdd:red"     "d73a4a" "Failing test written"
upsert_label "tdd:green"   "0e8a16" "Tests passing"

Milestone Bootstrap

# Idempotent milestone create
upsert_milestone() {
  local title="$1" due="$2" description="$3"
  EXISTING=$(gh api "repos/{owner}/{repo}/milestones" \
    --jq ".[] | select(.title == \"$title\") | .number" 2>/dev/null)
  if [ -n "$EXISTING" ]; then
    gh api "repos/{owner}/{repo}/milestones/$EXISTING" \
      -X PATCH \
      -f title="$title" \
      -f due_on="${due}T00:00:00Z" \
      -f description="$description"
  else
    gh api "repos/{owner}/{repo}/milestones" \
      -X POST \
      -f title="$title" \
      -f due_on="${due}T00:00:00Z" \
      -f description="$description"
  fi
}

# Example usage
upsert_milestone "v1.0" "2025-12-31" "Initial release"

Assign Labels to an Issue

# Add labels (idempotent — gh ignores if already present)
gh issue edit {issue_number} \
  --add-label "type:feature,priority:high,phase:discover"

# Remove a specific label
gh issue edit {issue_number} --remove-label "phase:discover"

Failure Modes

Condition Action
Label name collision (wrong color) gh label edit — update in place
Milestone already exists Patch via API — do not create duplicate
gh not authenticated Stop immediately. Do not retry.
Label with special characters URL-encode the label name in API calls

Logging

[gh-labels] UPSERT  label="type:feature"    color=0052cc
[gh-labels] SKIP    label="priority:high"   (unchanged)
[gh-milestones] CREATE  "v1.0"  due=2025-12-31
[gh-milestones] SKIP    "v1.0"  already exists
管理GitHub Projects v2看板,支持添加Issue为卡片、查询看板状态及更新字段值(如状态、类型)。优先使用gh CLI,未提供时回退至GraphQL API,需读取配置文件获取项目ID。
需要向GitHub项目看板添加Issue 需要更新项目看板的字段状态或选项
.opencode/skills/gh-projects/SKILL.md
npx skills add kinncj/Heimdall --skill gh-projects -g -y
SKILL.md
Frontmatter
{
    "name": "gh-projects",
    "description": "Manage GitHub Projects v2 board: add issues, update field values, and query board state. Use when managing project boards."
}

SKILL: gh-projects

Purpose

Manage GitHub Projects v2 board operations: add issues as cards, update field values (status, type, ADR required), and query board state. Uses gh project CLI where available; falls back to gh api graphql for operations the CLI does not expose.

Inputs

Field Source Example
project_number project.config.yaml 3
project_node_id project.config.yaml "PVT_kwDO..."
issue_node_id gh-issues skill output "I_kwDO..."
issue_number gh-issues skill output 42
field_name project field name "Status"
field_value target option name "In progress"

Read project config

PROJECT_NUMBER=$(python3 -c "
import sys
for line in open('project.config.yaml'):
    if 'project_number:' in line:
        print(line.split(':')[1].strip())
        break
")

PROJECT_NODE=$(python3 -c "
import sys
for line in open('project.config.yaml'):
    if 'project_node_id:' in line:
        print(line.split(':', 1)[1].strip())
        break
")

Add an Issue to the Project Board

# CLI (preferred)
gh project item-add "$PROJECT_NUMBER" \
  --owner "{owner}" \
  --url "https://github.com/{owner}/{repo}/issues/{issue_number}"

# GraphQL fallback (when CLI unavailable or for automation)
gh api graphql -f query='
  mutation($project: ID!, $content: ID!) {
    addProjectV2ItemById(input: {projectId: $project, contentId: $content}) {
      item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f content="{issue_node_id}" \
  --jq '.data.addProjectV2ItemById.item.id'

Save the returned item_id — required for field updates.

Get Field and Option IDs

Field updates require the field's node ID and the option's node ID (for single-select fields).

FIELDS=$(gh api graphql -f query='
  query($project: ID!) {
    node(id: $project) {
      ... on ProjectV2 {
        fields(first: 20) {
          nodes {
            ... on ProjectV2Field { id name }
            ... on ProjectV2SingleSelectField {
              id name
              options { id name }
            }
          }
        }
      }
    }
  }' \
  -f project="$PROJECT_NODE")

# Extract field ID by name
FIELD_ID=$(echo "$FIELDS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for f in d['data']['node']['fields']['nodes']:
    if f.get('name') == '{field_name}':
        print(f['id'])
        break
")

# Extract option ID by value (single-select fields)
OPTION_ID=$(echo "$FIELDS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for f in d['data']['node']['fields']['nodes']:
    if f.get('name') == '{field_name}':
        for opt in f.get('options', []):
            if opt['name'] == '{field_value}':
                print(opt['id'])
                break
")

Update a Single-Select Field (Status, Type, ADR Required)

gh api graphql -f query='
  mutation($project: ID!, $item: ID!, $field: ID!, $option: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $project
      itemId: $item
      fieldId: $field
      value: { singleSelectOptionId: $option }
    }) {
      projectV2Item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f item="{item_id}" \
  -f field="$FIELD_ID" \
  -f option="$OPTION_ID"

Update a Text Field (Epic, Specialist)

gh api graphql -f query='
  mutation($project: ID!, $item: ID!, $field: ID!, $value: String!) {
    updateProjectV2ItemFieldValue(input: {
      projectId: $project
      itemId: $item
      fieldId: $field
      value: { text: $value }
    }) {
      projectV2Item { id }
    }
  }' \
  -f project="$PROJECT_NODE" \
  -f item="{item_id}" \
  -f field="$FIELD_ID" \
  -f value="{text_value}"

Standard Field Updates by Pipeline Phase

Phase Field Value
Discover Status Todo
Architect Status In progress
Implement Status In progress
Validate Status In Review
Done Status Done

Query Board State

# List all items with status
gh project item-list "$PROJECT_NUMBER" \
  --owner "{owner}" \
  --format json \
  --jq '.items[] | {id, title: .content.title, status: .fieldValues[] | select(.field.name=="Status") | .name}'

Failure Modes

Condition Action
project_node_id missing from config Run maple project to bootstrap. Stop until resolved.
Item already on board Query before adding. gh project item-list and check content URL. Skip if present.
Field ID not found Re-fetch fields. Field names are case-sensitive.
Option ID not found List available options from field metadata. Do not guess.
GraphQL rate limit Wait 60 seconds. Retry once. Log and stop on second failure.

Logging

[gh-projects] ADD    #42  → project #{project_number}  item_id={id}
[gh-projects] UPDATE #42  field=Status  value="In progress"
[gh-projects] SKIP   #42  already on board
用于生成符合规范的Gherkin故事文件。自动处理ID分配、时间戳生成及文件名构造,提供标准模板并校验Gherkin语法规范,确保场景单一职责与标签完整,适用于产品负责人编写或更新用户故事。
需要编写新的Gherkin用户故事 需要更新现有的Gherkin故事文件 需要验证Gherkin文件的命名和格式规范性
.opencode/skills/gherkin-authoring/SKILL.md
npx skills add kinncj/Heimdall --skill gherkin-authoring -g -y
SKILL.md
Frontmatter
{
    "name": "gherkin-authoring",
    "description": "Produce valid, consistently named Gherkin story files with correct IDs, timestamps, and tags. Use when writing or updating story files."
}

SKILL: gherkin-authoring

Purpose

Produce valid, consistently named Gherkin story files. Handles ID allocation, timestamp generation, filename construction, tag formatting, and scenario validation. Used by the product-owner agent whenever a new story is written or updated.

Story File Naming Convention

<epic-slug>-<story-slug>-<timestamp>-<NNNN>.md
Part Format Example
epic-slug kebab-case, 2–4 words user-auth
story-slug kebab-case, 2–5 words reset-password
timestamp YYYYMMDDHHMMSS 20250416143000
NNNN 4-digit zero-padded, sequential within epic 0003

Full example: user-auth-reset-password-20250416143000-0003.md

Allocate the Next Story ID

# Count existing stories for an epic to get next sequential ID
EPIC="user-auth"
NEXT_ID=$(ls docs/stories/${EPIC}-* 2>/dev/null \
  | grep -oE '\-[0-9]{4}\.md$' \
  | grep -oE '[0-9]{4}' \
  | sort -n | tail -1 \
  | python3 -c "import sys; n=sys.stdin.read().strip(); print(f'{int(n)+1:04d}' if n else '0001')")
[ -z "$NEXT_ID" ] && NEXT_ID="0001"
echo "$NEXT_ID"

Generate Filename

EPIC="user-auth"
STORY="reset-password"
TS=$(date -u +"%Y%m%d%H%M%S")
ID="$NEXT_ID"   # from allocation step above
FILENAME="${EPIC}-${STORY}-${TS}-${ID}.md"
echo "$FILENAME"   # user-auth-reset-password-20250416143000-0001.md

Story File Template

Every story file must contain:

---
id: "{EPIC}-{NNNN}"
title: "{Human-readable story title}"
epic: "{epic-slug}"
priority: "high|medium|low|critical"
ui: false
adr_required: false
milestone: "{milestone name}"
labels:
  - "type:feature"
  - "priority:high"
  - "phase:discover"
issue_number: null
issue_url: null
---

## Story

**As a** {actor},
**I want** {capability},
**so that** {business outcome}.

## Acceptance Criteria

```gherkin
@story:{EPIC}-{NNNN} @epic:{epic-slug} @priority:{level}
Feature: {Feature title}

  Scenario: {scenario title}
    Given {precondition}
    When {action}
    Then {expected outcome}

Definition of Done

  • All Gherkin scenarios have passing step implementations
  • Unit tests written and passing
  • Integration tests written and passing (if applicable)
  • Code reviewed and approved
  • No regressions in related features
  • Documentation updated (if applicable)

ADR Links


## Gherkin Validation Rules

Before writing a scenario, verify:

1. **Single responsibility**: each `Scenario` tests exactly one behavior.
2. **Given-When-Then order**: never skip or reorder.
3. **No implementation detail in steps**: steps describe intent, not code.
4. **Declarative steps**: `Given the user is logged in` ✓ — `Given I call POST /auth/login` ✗
5. **Tags present**: `@story:`, `@epic:`, `@priority:` required on every Feature block.
6. **No `And` as first step**: `And` is only valid after a `Given`, `When`, or `Then`.

## Background Block (shared preconditions)

```gherkin
Feature: Password Reset

  Background:
    Given the user has a verified account
    And the account is not locked

  Scenario: Successful reset request
    When the user submits their email address
    Then a reset link is sent to that address

Use Background when 2+ scenarios share the same preconditions.

Scenario Outline (data-driven)

  Scenario Outline: Reject invalid email formats
    When the user submits "<email>"
    Then they see a validation error "<message>"

    Examples:
      | email        | message                  |
      | notanemail   | Invalid email format     |
      | @nodomain    | Invalid email format     |
      | a@b          | Domain must have TLD     |

Tags Reference

Tag Required Description
@story:{id} Yes Links scenario to story file
@epic:{slug} Yes Groups stories by epic
@priority:{level} Yes critical, high, medium, low
@wip No Work in progress — excluded from CI by default
@ui No Requires browser/UI driver
@integration No Requires external services
@smoke No Runs in production smoke suite

Failure Modes

Condition Action
Story ID collision (same NNNN exists) Re-run allocation step. Never overwrite an existing file.
docs/stories/ does not exist Create it: mkdir -p docs/stories/
Scenario has no When Invalid — split into two scenarios or add action step
Missing required tags Add tags before writing the file
Title exceeds 72 characters Shorten. Titles appear in issue headings and PR titles.

Logging

[gherkin] ALLOCATE  epic=user-auth  next_id=0003
[gherkin] CREATE    docs/stories/user-auth-reset-password-20250416143000-0003.md
[gherkin] VALIDATE  scenarios=3  tags=ok  structure=ok
提供GitHub CLI (gh) 的完整操作指南,涵盖仓库上下文获取、Issue全生命周期管理(创建/查询/更新/评论/关闭)及Pull Request处理(创建/审查/合并),用于自动化和简化GitHub交互流程。
需要查看或管理GitHub Issue时 需要创建、审查或合并Pull Request时 需要查询仓库状态、分支或Actions信息时 需要使用GitHub CLI执行任务自动化时
.opencode/skills/github-cli/SKILL.md
npx skills add kinncj/Heimdall --skill github-cli -g -y
SKILL.md
Frontmatter
{
    "name": "github-cli",
    "description": "Comprehensive gh CLI reference — issues, tasks, PRs, repos, branches, Actions, search, and project board. Use whenever interacting with GitHub."
}

SKILL: GitHub CLI

Repo Context (always run first)

# Who am I, what repo am I in
gh auth status
gh repo view --json name,owner,defaultBranchRef,url,isPrivate

# Open issues + PRs at a glance
gh issue list --state open --limit 20 --json number,title,labels,assignees
gh pr list --state open --json number,title,headRefName,statusCheckRollup

Issues as Tasks

Create

# Feature story
gh issue create \
  --title "feat: {title}" \
  --body-file docs/stories/{slug}/Story.md \
  --label "type:feature,priority:high,phase:discover" \
  --milestone "v1.0" \
  --assignee "@me"

# Bug
gh issue create \
  --title "fix: {title}" \
  --body "## Steps to reproduce\n{steps}\n\n## Expected\n{expected}\n\n## Actual\n{actual}" \
  --label "type:bug,priority:high"

# Task (sub-work, no story file)
gh issue create \
  --title "task: {title}" \
  --body "{description}" \
  --label "type:task" \
  --assignee "@me"

Read

gh issue view {number}
gh issue view {number} --json number,title,state,body,labels,assignees,comments,milestone

# Search
gh issue list --search "label:blocked" --state open
gh issue list --search "assignee:@me is:open"
gh issue list --search "phase:implement" --label "tdd:red"
gh issue list --state open --label "type:feature" --milestone "v1.0"

Update

# Phase transition
gh issue edit {number} \
  --add-label "phase:architect" \
  --remove-label "phase:discover"

# Assign / unassign
gh issue edit {number} --add-assignee "@me"
gh issue edit {number} --remove-assignee "{login}"

# Change milestone
gh issue edit {number} --milestone "v2.0"

# Block / unblock
gh issue edit {number} --add-label "blocked"
gh issue edit {number} --remove-label "blocked"

Comment

gh issue comment {number} --body "{message}"

# Phase lifecycle comments (standard format)
gh issue comment {number} --body "Phase {N} {NAME}: starting. Target artifacts: {paths}"
gh issue comment {number} --body "Phase {N} {NAME}: complete. Artifacts: {paths}"
gh issue comment {number} --body "RED: Failing test at {path} — {message}"
gh issue comment {number} --body "GREEN: {count} tests passing."
gh issue comment {number} --body "BLOCKED: {agent} failed 3x on {task}. Human required."

Close

gh issue close {number} --comment "Acceptance criteria passing. Validation: {summary}"
gh issue close {number} --reason "not planned"

Pull Requests

# Create PR (always draft first)
gh pr create \
  --title "feat: {description}" \
  --body "## Summary\n{summary}\n\nCloses #{issue_number}" \
  --base main \
  --draft

# Mark ready when all gates pass
gh pr ready {number}

# View PR + CI status
gh pr view {number} --json number,title,state,reviews,statusCheckRollup,files

# Request review
gh pr edit {number} --add-reviewer "{login}"

# Check CI on PR
gh pr checks {number}

# Merge (squash — preferred)
gh pr merge {number} --squash --subject "feat: {description} (#{issue_number})" --delete-branch

# Review
gh pr review {number} --approve --body "LGTM"
gh pr review {number} --request-changes --body "{feedback}"

Branches

# List branches
gh api repos/{owner}/{repo}/branches --jq '.[].name'

# Create via API (when not in git context)
gh api repos/{owner}/{repo}/git/refs \
  -f ref="refs/heads/feat/{slug}" \
  -f sha="$(gh api repos/{owner}/{repo}/git/ref/heads/main --jq '.object.sha')"

# Delete remote branch
gh api -X DELETE repos/{owner}/{repo}/git/refs/heads/feat/{slug}

# Branch protection status
gh api repos/{owner}/{repo}/branches/main/protection

Actions / CI

# List recent runs
gh run list --limit 10
gh run list --workflow ci.yml --branch main --limit 5

# Watch a run live
gh run watch {run-id}

# View failed run details
gh run view {run-id} --log-failed

# Trigger workflow manually
gh workflow run {workflow-file} --ref main

# Re-run failed jobs only
gh run rerun {run-id} --failed

# Download artifact
gh run download {run-id} --name {artifact-name} --dir ./artifacts

Repo Inspection

# Full repo metadata
gh repo view --json name,owner,description,topics,visibility,defaultBranchRef,\
licenseInfo,stargazerCount,forkCount,createdAt,updatedAt

# Collaborators
gh api repos/{owner}/{repo}/collaborators --jq '.[].login'

# Repo settings
gh api repos/{owner}/{repo} --jq '{private:.private,hasIssues:.has_issues,hasWiki:.has_wiki}'

# Topics
gh repo edit --add-topic "{topic}"

# Clone URL
gh repo view --json sshUrl,url

# Releases
gh release list --limit 5
gh release view {tag}

Labels & Milestones

# Bootstrap MAPLE labels (idempotent)
bash scripts/bootstrap-labels.sh

# List labels
gh label list

# Create label
gh label create "type:task" --color "0075ca" --description "Work item"

# Create milestone
gh api repos/{owner}/{repo}/milestones \
  -f title="v1.0" \
  -f description="First release" \
  -f due_on="2026-06-01T00:00:00Z"

# List milestones
gh api repos/{owner}/{repo}/milestones --jq '.[] | {number:.number,title:.title,open:.open_issues}'

Search

# Search code in repo
gh search code "{query}" --repo {owner}/{repo} --json path,url

# Search issues across GitHub
gh search issues "{query}" --repo {owner}/{repo} --json number,title,state

# Search PRs
gh search prs "{query}" --repo {owner}/{repo} --state open

Issue Lifecycle (MAPLE Standard)

Event Label added Label removed Comment
Story created phase:discover "Phase 1 DISCOVER: story created."
Architect done phase:architect phase:discover "Phase 2 ARCHITECT: design complete."
Plan done phase:plan phase:architect "Phase 3 PLAN: plan.md ready."
Infra done phase:infra phase:plan "Phase 4 INFRA: containers healthy."
Implement start phase:implement,in-progress phase:infra "Phase 5 IMPLEMENT: TDD loop starting."
TDD red tdd:red "RED: failing test at {path}"
TDD green tdd:green tdd:red "GREEN: {n} tests passing."
Validate pass phase:validate,validated phase:implement "Phase 6 VALIDATE: 100% pass."
Document done phase:document phase:validate "Phase 7 DOCUMENT: docs complete."
PR created ready-for-review phase:document "Phase 8: PR #{pr} created."
Blocked blocked "BLOCKED: {agent} failed 3x. Human needed."
Unblocked blocked "Unblocked: {resolution}"

Failure Modes

Condition Action
Not authenticated gh auth login — do not retry ops
Label missing Create via gh label create before using
Milestone missing Create via gh api milestones before using
Rate limited Wait 60s, retry once; if fails again, stop
Issue already exists gh issue list --search "{title}" --state all — return existing number
gh not found Stop. Output: "gh CLI not available. Install from https://cli.github.com"
去除文本中的AI生成痕迹,使文档、注释和提交信息更自然。支持通过命令或对话调用,可校准个人风格,检测29种AI写作模式并优化表达。
生成文档后润色 提交代码前检查 让AI辅助文本更自然 品牌语调一致性调整
.opencode/skills/humanizer/SKILL.md
npx skills add kinncj/Heimdall --skill humanizer -g -y
SKILL.md
Frontmatter
{
    "name": "humanizer",
    "description": "Remove AI-isms and artificial language patterns from text. Makes documentation, comments, commit messages, and prose sound more natural and human. Based on Wikipedia's \"Signs of AI writing\" patterns."
}

humanizer skill

Polish text by removing detectable AI-generated writing patterns. Ideal for finalizing documentation, commit messages, comments, and any prose before merging.

When to use

  • After @docs generates feature documentation
  • Before committing code (check commit messages and comments)
  • Polishing README updates, CHANGELOG entries, or runbooks
  • Making AI-assisted prose sound more natural
  • Manual voice calibration for brand consistency

How to invoke

/humanizer

[paste your text here]

Or ask directly in chat:

Please humanize this text: [your text]

Voice calibration (optional)

Provide a sample of your own writing for the skill to match your style:

/humanizer

Here's a sample of my writing for voice matching:
[paste 2-3 paragraphs of your own writing]

Now humanize this text:
[paste AI text to humanize]

What it detects

The skill checks for 29 AI-writing patterns, including:

  • Significance inflation — "marking a pivotal moment in the evolution of..." → "was established in 1989"
  • Notability name-dropping — Lists of citations → specific examples with context
  • Superficial -ing phrases — "symbolizing, reflecting, showcasing" → active explanations
  • Promotional language — Corporate jargon → straightforward description
  • Overuse of 'indeed' — Filler words → tighter prose
  • Transition abuse — Excessive "Furthermore, In conclusion..." → natural flow
  • Hedging redundancy — "arguably, in some sense, it could be argued..." → clarity
  • Rare word stacking — Thesaurus abuse → common vocabulary
  • False expert mode — Generic expertise → grounded examples
  • Generalist fluff — "many fields, various domains..." → specific scope

Integration with MAPLE

  • Auto-call after Phase 7 (DOCUMENT) if humanize: true in feature story frontmatter
  • Manual: invoke at any phase before merge
  • Works across all harnesses (Claude Code, OpenCode, Cursor, Copilot CLI)

Output

Returns a humanized version of your text with:

  1. First pass: Remove identified AI patterns
  2. Final audit: "Obviously AI generated?" check
  3. Second rewrite: Catch lingering AI-isms

Further reading

提供Jupyter Notebook最佳实践,包括标准单元格结构、使用Papermill实现参数化执行、通过nbformat编程创建笔记本、利用nbconvert导出格式,以及确保Git版本控制的整洁与可复现性。
处理.ipynb文件时优化结构 需要参数化运行Notebook 编程生成或修改Notebook 导出Notebook为HTML或PDF 清理Notebook输出以提交Git
.opencode/skills/jupyter-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill jupyter-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "jupyter-patterns",
    "description": "Apply best practices for Jupyter notebooks: cell ordering, reproducibility, parameterisation. Use when working with .ipynb files."
}

SKILL: Jupyter Notebook Patterns

Notebook Structure

Cells should follow this order:

  1. Setup — imports, configuration, constants
  2. Data Loading — load raw data with validation
  3. EDA — exploratory data analysis, distributions, correlations
  4. Preprocessing — cleaning, feature engineering
  5. Modeling — model training and evaluation
  6. Visualization — charts and figures
  7. Conclusions — findings summary, next steps

Parameterized Execution with Papermill

# In notebook cell tagged with "parameters":
# Click: View -> Cell Toolbar -> Tags -> add "parameters" tag

dataset = "data/train.csv"  # papermill will override this
output_dir = "outputs"
n_estimators = 100
random_state = 42
# Execute with papermill
papermill input.ipynb output.ipynb \
  -p dataset "data/test.csv" \
  -p n_estimators 200 \
  -p random_state 0

Programmatic Notebook Creation

import nbformat as nbf

nb = nbf.v4.new_notebook()
nb.cells = [
    nbf.v4.new_markdown_cell("# Analysis: {Title}"),
    nbf.v4.new_code_cell("import pandas as pd\nimport numpy as np"),
    nbf.v4.new_code_cell("df = pd.read_csv('data.csv')\ndf.head()"),
]

with open('analysis.ipynb', 'w') as f:
    nbf.write(nb, f)

Export

# To HTML (with outputs)
jupyter nbconvert --to html --execute notebook.ipynb

# To PDF
jupyter nbconvert --to pdf --execute notebook.ipynb

# Execute in place
jupyter nbconvert --to notebook --execute --inplace notebook.ipynb

Git Hygiene

# Clear outputs before commit
jupyter nbconvert --to notebook --ClearOutputPreprocessor.enabled=True \
  --inplace notebook.ipynb

# Or use nbstripout (installs as git filter)
pip install nbstripout
nbstripout --install

Rules

  • Restart kernel and run all cells before committing.
  • Clear all outputs before git commit.
  • Tag parameter cells for papermill.
  • Each notebook should be self-contained and reproducible.
  • Include random_state parameter for reproducibility.
  • Use relative paths for data files.
依据Karpathy四大原则审计代码变更,自动在实施阶段后触发或手动调用。生成合规评分报告,若未达标则阻止流程推进,确保代码简洁、精准且目标导向。
Phase 5 IMPLEMENT完成后自动触发 用户在聊天中发送 /karpathy-audit 或 @karpathy-audit
.opencode/skills/karpathy-audit/SKILL.md
npx skills add kinncj/Heimdall --skill karpathy-audit -g -y
SKILL.md
Frontmatter
{
    "name": "karpathy-audit",
    "description": "Audit code changes against Karpathy's 4 principles (Think Before Coding, Simplicity First, Surgical Changes, Goal-Driven Execution). Auto-called after Phase 5 IMPLEMENT; can also be invoked manually. Produces scored compliance report and blocks advancement if threshold not met."
}

karpathy-audit skill

Audit PRs and code changes against Andrej Karpathy's 4 principles for reducing LLM coding mistakes.

When to use

  • Auto-invoked after Phase 5 (IMPLEMENT) before advancing to Phase 6 (VALIDATE)
  • Manual invocation: /karpathy-audit or @karpathy-audit in chat
  • At any phase when you want to assess code quality against the principles

The 4 Principles

1. Think Before Coding

Don't assume. Don't hide confusion. Surface tradeoffs.

  • State assumptions explicitly. If uncertain, ask.
  • Present multiple interpretations—don't pick silently.
  • If a simpler approach exists, say so. Push back when warranted.
  • Stop when confused. Name what's unclear and ask.

2. Simplicity First

Minimum code that solves the problem. Nothing speculative.

  • No features beyond what was asked.
  • No abstractions for single-use code.
  • No "flexibility" or "configurability" that wasn't requested.
  • No error handling for impossible scenarios.
  • If 200 lines could be 50, rewrite it.

3. Surgical Changes

Touch only what you must. Clean up only your own mess.

  • Don't "improve" adjacent code, comments, or formatting.
  • Don't refactor things that aren't broken.
  • Match existing style, even if you'd do it differently.
  • Only remove imports/variables/functions that YOUR changes made unused.

4. Goal-Driven Execution

Define success criteria. Loop until verified.

  • Transform tasks into verifiable goals: tests before code.
  • State a brief plan with explicit success criteria.
  • Loop independently until verified.

How to invoke

Auto-call (Phase 5 → Phase 6 gate): Orchestrator automatically calls this skill after IMPLEMENT phase to audit the diff.

Manual call:

/karpathy-audit

or

@karpathy-audit

Audit output

The skill produces a compliance report written to .claude/state/karpathy-report.json:

{
  "phase": "IMPLEMENT",
  "timestamp": "2026-05-18T21:25:00Z",
  "spec_file": "docs/stories/user-auth/Story.md",
  "audit": {
    "think_before_coding": {
      "score": 85,
      "violations": ["Assumed DB schema without asking"],
      "evidence": ["Line 42: const User = model('User')"]
    },
    "simplicity_first": {
      "score": 92,
      "violations": [],
      "evidence": ["Reduced 180→120 lines in refactor"]
    },
    "surgical_changes": {
      "score": 78,
      "violations": ["Modified logging in unrelated file"],
      "scope_creep_files": ["src/logging/index.ts"],
      "out_of_spec": true
    },
    "goal_driven": {
      "score": 100,
      "violations": [],
      "evidence": ["All tests passing. RED→GREEN→REFACTOR complete."]
    },
    "overall": 89,
    "gate_decision": "PASS_WITH_APPROVAL",
    "recommendation": "Score 89: Pass scope and simplicity checks. Recommend human approval before advancing to Phase 6."
  }
}

Scoring & Gate Decisions

Overall Score Decision Action
≥90 🟢 PASS Auto-advance to next phase
70-89 🟡 PASS_WITH_APPROVAL Pause. Require human approval to advance.
<70 🔴 FAIL BLOCK. Orchestrator halts. Requires remediation + re-audit.

Scope creep detection

Compares two sources:

  1. Original spec — From docs/stories/{story}/Story.md (required scope)
  2. PR diff — Files and line changes in the current PR

Violations flagged:

  • Files modified outside the spec boundary
  • Feature branches added beyond the story requirement
  • Unrelated cleanup/refactoring

Phase 5 → Phase 6 Gate

Orchestrator behavior:

Phase 5 (IMPLEMENT) complete
  ↓
Auto-call: /karpathy-audit
  ↓
Analyze spec + PR diff + 4 principles
  ↓
Write `.claude/state/karpathy-report.json`
  ↓
Score ≥90?      → Auto-advance to Phase 6
Score 70-89?    → Pause. Display report. Require [a]pprove or [c]cancel
Score <70?      → HALT. Show violations. Require /karpathy-audit re-run after fixes

Dashboard integration

The TUI displays:

  • Phase 5 detail view — Shows karpathy-report.json if present
  • [P] overlay (Pipeline pane) — Per-principle scores + violations
  • Red blocking indicator — If score <70, blocks manual phase advance
  • Approval gate — If 70-89, shows "awaiting approval" state

Remediation workflow

If audit fails or scores <70:

1. Orchestrator shows violation details
2. Specialist agent re-assesses work against the principle
3. Fix or remove out-of-scope code
4. Manually re-run: /karpathy-audit
5. If all 4 principles ≥70 now, approve advancement

Example invocation

User: /feature "add password reset via email"
Orchestrator: [Phase 1-5 complete]
Orchestrator: Auditing against Karpathy principles...
karpathy-audit: Running audit...
[Analyze spec vs PR diff]
[Score each principle]
karpathy-audit: 📋 Report written to .claude/state/karpathy-report.json
karpathy-audit: 🟡 Score 82/100 — PASS_WITH_APPROVAL
karpathy-audit: ⚠️ Scope creep: modified src/logging (out of spec)

Dashboard: [Shows approval gate, awaiting [a] key]
User: a  [approve]
Dashboard: [Phase 6 VALIDATE begins]

Further reading

提供Kubernetes和Kustomize最佳实践,包括标准目录结构、Deployment配置(含安全与探针)、PDB及HPA模板。适用于编写或审查k8s清单文件,并附带干运行验证工作流以确保部署正确性。
编写或审查 Kubernetes YAML 清单 使用 Kustomize 管理环境覆盖 配置 PodDisruptionBudget 或 HPA 执行 k8s 资源验证
.opencode/skills/kubernetes-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill kubernetes-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "kubernetes-patterns",
    "description": "Apply Kubernetes and Kustomize patterns for deployments, services, and overlays. Use when writing or reviewing k8s manifests."
}

SKILL: Kubernetes Patterns

Kustomize Structure

infra/k8s/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   ├── kustomization.yaml
    │   └── patches/
    ├── staging/
    │   ├── kustomization.yaml
    │   └── patches/
    └── prod/
        ├── kustomization.yaml
        └── patches/

Deployment with Required Fields

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {app-name}
  labels:
    app: {app-name}
spec:
  replicas: 2
  selector:
    matchLabels:
      app: {app-name}
  template:
    metadata:
      labels:
        app: {app-name}
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
      containers:
        - name: {app-name}
          image: {image}:{tag}
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 20
          startupProbe:
            httpGet:
              path: /health
              port: 3000
            failureThreshold: 30
            periodSeconds: 10

PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {app-name}-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: {app-name}

HPA

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {app-name}-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {app-name}
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Validation Workflow

# Always validate before applying
kubectl apply --dry-run=client -f {manifest}
kustomize build overlays/dev | kubectl apply --dry-run=client -f -
helm template {release} {chart} | kubectl apply --dry-run=client -f -
用于生成和验证Mermaid图表(组件、序列、ER、部署、状态机),以记录系统架构。需遵循节点数限制、语法有效性及编辑器测试规则,确保图示清晰准确。
需要生成系统架构图 需要绘制数据流或交互流程 需要展示数据库实体关系 需要描述部署拓扑结构 需要可视化状态转换逻辑
.opencode/skills/mermaid-diagrams/SKILL.md
npx skills add kinncj/Heimdall --skill mermaid-diagrams -g -y
SKILL.md
Frontmatter
{
    "name": "mermaid-diagrams",
    "description": "Generate and validate Mermaid diagrams (component, sequence, ER, deployment, state machine) for features. Use when documenting architecture."
}

SKILL: Mermaid Diagrams

Required Diagrams Per Feature

  1. Component diagram (graph TB)
  2. Sequence diagram (sequenceDiagram)
  3. ER diagram (erDiagram)
  4. Deployment diagram (graph TB with deployment focus)
  5. State machine (stateDiagram-v2) — where applicable

Rules

  • Maximum 30 nodes per diagram.
  • All diagrams must be syntactically valid.
  • Test diagrams in the Mermaid Live Editor before committing.
  • Use descriptive node labels.

Component Diagram

graph TB
    subgraph "Frontend"
        UI["React App"]
    end
    subgraph "Backend"
        API["REST API"]
        DB[("PostgreSQL")]
        Cache[("Redis")]
    end
    UI -->|"HTTPS"| API
    API --> DB
    API --> Cache

Sequence Diagram

sequenceDiagram
    actor U as User
    participant F as Frontend
    participant A as API
    participant D as Database

    U->>F: Action
    F->>A: POST /api/resource
    A->>D: INSERT
    D-->>A: 201 Created
    A-->>F: {id, data}
    F-->>U: Success

ER Diagram

erDiagram
    USER {
        uuid id PK
        string email UK
        timestamp created_at
    }
    ORDER {
        uuid id PK
        uuid user_id FK
        decimal total
        timestamp created_at
    }
    USER ||--o{ ORDER : "places"

State Machine

stateDiagram-v2
    [*] --> Pending
    Pending --> Processing: payment received
    Processing --> Completed: fulfilled
    Processing --> Failed: error
    Failed --> Pending: retry
    Completed --> [*]

Deployment Diagram

graph TB
    subgraph "Vercel Edge"
        FE["Next.js App"]
        EF["Edge Functions"]
    end
    subgraph "AWS"
        LB["Load Balancer"]
        APP["App Servers"]
        RDS[("RDS PostgreSQL")]
        EC["ElastiCache Redis"]
    end
    FE --> EF
    EF --> LB
    LB --> APP
    APP --> RDS
    APP --> EC
根据已审批的线框图和设计令牌,使用项目指定的UI技术栈(如Mantine、Tailwind或HTML)生成高保真UI组件代码模拟物。支持Web和终端界面目标,需通过预检确保线框图状态及令牌文件存在。
需要基于线框图生成高保真UI组件代码时 实现已审批的设计方案并输出可运行代码时
.opencode/skills/mockup/SKILL.md
npx skills add kinncj/Heimdall --skill mockup -g -y
SKILL.md
Frontmatter
{
    "name": "mockup",
    "description": "Scaffold high-fidelity UI component mockups using the project UI stack consuming approved wireframes. Use when implementing approved wireframes."
}

SKILL: mockup

Purpose

Scaffold high-fidelity UI component mockups using the project's declared UI stack (Mantine, Tailwind/shadcn, or plain HTML). Mockups consume approved wireframes and design tokens. Output is runnable code — not a design file. Human approval required before the mockup is treated as the implementation target.

Inputs

Field Source Example
story_file path to story markdown docs/stories/auth-reset-0001.md
wireframe_file path to approved wireframe docs/design/wireframes/auth-reset-0001.wireframe.md
tokens_file canonical token file docs/design/identity/tokens.json
stack project.config.yaml react-mantine | react-tailwind | html

Supported Stacks (web target)

Stack value Framework Token source
react-mantine @mantine/core mantine.theme.ts
react-tailwind Tailwind CSS + shadcn/ui tailwind.tokens.js
react-shadcn shadcn/ui + Tailwind tailwind.tokens.js
html Vanilla HTML + tokens.css tokens.css

Outputs

File Location Targets
<story-id>.mockup.md docs/design/mockups/ web + tui — metadata + approval (tui: also the terminal render + lipgloss styles)
<story-id>.mockup.tsx docs/design/mockups/ web only — React stacks
<story-id>.mockup.html docs/design/mockups/ web only — html stack

Target awareness

design.target (project.config.yaml, default web):

  • web — generate the code mockup (.tsx/.html) for the configured ui_library, plus .mockup.md.
  • tui — generate ONLY <id>.mockup.md: a fenced monospace terminal render at the target width covering default/selected/empty/error/loading states, followed by a "Styles" section mapping each region to lipgloss Foreground/Background/Border/Padding taken from docs/design/identity/terminal-theme.json. Skip the stack detector and the React templates below.

Pre-flight Checks

Before generating:

WIREFRAME="docs/design/wireframes/${STORY_ID}.wireframe.md"
TOKENS="docs/design/identity/tokens.json"

# 1. Wireframe must be approved
STATUS=$(python3 -c "
import re
m = re.search(r'^status:\s*(\w+)', open('$WIREFRAME').read(), re.MULTILINE)
print(m.group(1) if m else 'draft')
")
[ "$STATUS" = "approved" ] || { echo "BLOCKED: wireframe not approved"; exit 1; }

# 2. Tokens must exist
[ -f "$TOKENS" ] || { echo "BLOCKED: tokens.json missing — run visual-identity skill first"; exit 1; }

Mockup Template: react-mantine

// AUTO-GENERATED MOCKUP — docs/design/mockups/{story-id}.mockup.tsx
// Story: {story title}
// Wireframe: {wireframe file}
// Tokens: mantine.theme.ts
// Status: draft — awaiting approval

import { MantineProvider, Stack, TextInput, Button, Text, Alert } from '@mantine/core';
import { theme } from '../../docs/design/identity/mantine.theme';
import { IconAlertCircle } from '@tabler/icons-react';

interface Props {
  error?: string;
  onSubmit: (email: string) => void;
}

export function {ComponentName}({ error, onSubmit }: Props) {
  const [email, setEmail] = useState('');

  return (
    <MantineProvider theme={theme}>
      <Stack gap="md" maw={400} mx="auto" mt="xl">
        <Text fw={700} size="xl">{Screen Title}</Text>

        <TextInput
          label="Email"
          placeholder="you@example.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          error={error}
        />

        {error && (
          <Alert icon={<IconAlertCircle />} color="red" variant="light">
            {error}
          </Alert>
        )}

        <Button fullWidth onClick={() => onSubmit(email)}>
          Send Reset Link
        </Button>
      </Stack>
    </MantineProvider>
  );
}

Mockup Template: react-tailwind

// AUTO-GENERATED MOCKUP — docs/design/mockups/{story-id}.mockup.tsx
import { useState } from 'react';

interface Props {
  error?: string;
  onSubmit: (email: string) => void;
}

export function {ComponentName}({ error, onSubmit }: Props) {
  const [email, setEmail] = useState('');

  return (
    <div className="max-w-sm mx-auto mt-16 space-y-4">
      <h1 className="text-2xl font-bold text-foreground">{Screen Title}</h1>

      <div>
        <label className="block text-sm font-medium mb-1">Email</label>
        <input
          type="email"
          className={`w-full border rounded-md px-3 py-2 text-sm ${error ? 'border-error' : 'border-border'}`}
          placeholder="you@example.com"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        {error && <p className="text-error text-sm mt-1">{error}</p>}
      </div>

      <button
        className="w-full bg-brand-primary text-white rounded-md py-2 font-medium hover:opacity-90"
        onClick={() => onSubmit(email)}
      >
        Send Reset Link
      </button>
    </div>
  );
}

Mockup Metadata File

Always write a .mockup.md alongside the code file:

---
story_id: "{story_id}"
wireframe: "docs/design/wireframes/{story_id}.wireframe.md"
stack: "{stack}"
status: draft        # draft | approved | rejected
approved_by: null
approved_at: null
---

## Mockup: {story title}

### States

- Default
- Error: {describe}
- Success: {describe}
- Loading: {describe if applicable}

### Token Usage

- Primary action: `color.brand.primary`
- Error text: `color.semantic.error`
- Border: `color.surface.border`

### Approval

- [ ] Matches approved wireframe
- [ ] Design tokens applied correctly
- [ ] All interaction states present
- [ ] Approved by product owner / UX lead

Failure Modes

Condition Action
Wireframe not approved Stop. Output BLOCKED. Do not generate mockup.
tokens.json missing Stop. Run visual-identity first.
Stack unknown Default to html. Log warning.
Mockup exists and is approved Do not overwrite. Log SKIP — approved mockup locked.
Component name collision Append story ID suffix to component name.

Logging

[mockup] CREATE   docs/design/mockups/auth-reset-0001.mockup.tsx  stack=react-mantine
[mockup] CREATE   docs/design/mockups/auth-reset-0001.mockup.md   status=draft
[mockup] BLOCKED  auth-reset-0001  wireframe not approved
[mockup] SKIP     auth-reset-0001  mockup approved — locked
通用调度器,用于执行Taffy工作流、本地技能或子智能体。按优先级解析目标,支持从skills.sh注册表安装缺失技能。全程记录状态以支持实时进度监控,并强制遵循特定规范文件。
需要运行指定的Taffy工作流 需要调用本地或远程技能 需要委派任务给子智能体 /pipeline-runner命令
.opencode/skills/pipeline-runner/SKILL.md
npx skills add kinncj/Heimdall --skill pipeline-runner -g -y
SKILL.md
Frontmatter
{
    "name": "pipeline-runner",
    "description": "Universal dispatcher: run a named taffy workflow (.claude\/taffy\/<name>.yaml), a skill (\/skill-name), or a sub-agent (@agent-name). Falls back to skills.sh registry when a skill is not found locally. Tracks all runs in .claude\/state\/maple.json so the maple TUI shows live progress."
}

SKILL: pipeline-runner

What It Does

Dispatches any named workflow, skill, or agent from a single entry point. Resolution order:

  1. Taffy workflow — look for .claude/taffy/<name>.yaml; if found, execute each stage in order
  2. Skill (local) — look for .claude/skills/<name>/; if found, invoke the skill
  3. Agent — look for .claude/agents/<name>.md; if found, delegate to @<name>
  4. Skill (registry) — if not found locally, try to install from skills.sh, then retry step 2

Pipeline state is written to .claude/state/maple.json at every transition.

Usage

/pipeline-runner <name>

Examples:

/pipeline-runner new-ui-feature
/pipeline-runner api-endpoint
/pipeline-runner implement-stories
/pipeline-runner tdd-workflow
/pipeline-runner orchestrator

List available taffy workflows:

ls .claude/taffy/*.yaml | grep -v schema

List available skills:

ls .claude/skills/

Dispatch Protocol

Step 1: Resolve the target

# Check taffy first
[ -f ".claude/taffy/<name>.yaml" ] && dispatch=taffy
# Then local skill
[ -d ".claude/skills/<name>" ] && dispatch=skill
# Then agent
[ -f ".claude/agents/<name>.md" ] && dispatch=agent
# Fallback: fetch from skills.sh registry, then retry
if [ -z "$dispatch" ] && command -v npx &>/dev/null; then
  echo "pipeline-runner: '<name>' not found locally — checking skills.sh…"
  npx --yes skills add kinncj/maple@<name> -a claude-code -y 2>/dev/null \
    || npx --yes skills add <name> -a claude-code -y 2>/dev/null \
    || true
  [ -d ".claude/skills/<name>" ] && dispatch=skill
fi

If nothing matches after the registry fallback, report: pipeline-runner: no taffy workflow, skill, or agent named '<name>' (also checked skills.sh registry)

Step 2: Initialise state

Write to .claude/state/maple.json (merge — do not overwrite unowned fields):

{
  "taffy": "<name>",
  "stage": "<first-stage or skill-name>",
  "status": "RUNNING",
  "awaiting_approval": null,
  "started_at": "<iso8601>",
  "updated_at": "<iso8601>"
}

Step 2b: Runtime policy enforcement (mandatory)

Before any stage execution, read and enforce:

  • Harness-specific root markdown:
    • Claude harness → CLAUDE.md
    • OpenCode harness → OPENCODE.md
    • Cursor harness → CURSOR.md
    • Copilot harness → COPILOT.md
  • AGENTS.md
  • .github/copilot-instructions.md
  • .github/instructions/stories.instructions.md (when touching story files)

When the launch prompt contains a <maple-gherkin-handoff> block:

  1. Treat it as hard scope: implement only listed story paths / IDs.
  2. Do not run Spec-Kit or regenerate stories.
  3. Preserve the repository's current Cucumber stack:
    • If generated stories include cucumber/*_steps.py, use Python behave-style steps.
    • Do not introduce TypeScript @cucumber/cucumber unless the repository already uses it as the active standard.
  4. Keep BusinessRepo structure and phase gates exactly as defined by instruction files.
  5. Treat test layout as mandatory:
    • Gherkin feature files must live under /tests/features/.
    • Step definitions must use the repository's active Cucumber stack and live under /tests.
    • Do not place acceptance tests under /app or story directories.
  6. Enforce module boundaries independent of language:
    • Runtime/source files must not import from docs/, .github/, or .claude/.
    • Copying/adapting approved artifact content into app/test source is allowed.
    • Design/spec artifacts are references, not runtime code dependencies.

Step 3a: Taffy workflow execution

Load .claude/taffy/<name>.yaml, parse stages:, resolve depends_on order.

If the workflow defines an orchestrator-kickoff stage, execute it first before all other stages. This stage must publish the initial plan and heartbeat cadence.

For each stage:

when: guard:

  • when: ui:true — skip if story has ui: false, unless .claude/state/maple.json has force_ui: true with launch_source: maple-x (MAPLE [x] quick-launch override).
  • when: ui:false — skip if story has ui: true
  • when: always — always run
  • UI-related scope includes web, mobile, desktop, and TUI stories. Any such run must include design-review human-approval stages before implementation phases.

depends_on: all listed stages must be DONE before this one starts.

Dispatch:

  • agent: <name> → delegate to @<name> with current story context
  • skill: <name> → invoke the skill
  • pipeline: standard → run the full 8-phase orchestrator pipeline

After each stage: update maple.json with current stage + RUNNING.

Progress heartbeats (mandatory):

  • Send an immediate kickoff status before the first long-running tool/agent call.
  • While a taffy run is active, send a concise progress update at least every 60-120 seconds.
  • On each heartbeat, refresh maple.json updated_at and current stage.
  • Every heartbeat must include concrete progress evidence:
    • changed files/artifacts since last update (explicit paths), or
    • a specific blocker that prevented changes.
  • Use this status format:
    • Progress: <stage / phase>
    • Done since last update: <brief>
    • Current action: <brief>
    • Blockers: <none or blocker>
    • Next update: <ETA>
  • Do not send heartbeat-only timestamp churn with no artifact/blocker details.
  • If a stage requires writing artifacts and write access/tools are unavailable, set maple.json to FAILED with an explicit error and stop.
  • If blocked/waiting, report what is pending and continue heartbeats until unblocked.

Completion artifact gate (mandatory):

  • Before marking DONE, verify the run produced concrete story-linked artifacts under the BusinessRepo layout.
  • Required for implementation runs:
    • application changes in /app (or existing domain folders),
    • tests in /tests (unit/integration/e2e as applicable),
    • Gherkin assets in /tests/features plus matching step implementations.
  • Boundary check:
    • fail the run if generated runtime code imports paths under docs/, .github/, or .claude/.
  • If required test/gherkin artifacts are missing, set maple.json to FAILED and report missing paths explicitly.

Step 3b: Skill invocation

Invoke the skill directly. Update maple.json on start and completion.

Step 3c: Agent delegation

Delegate to @<name>. Update maple.json on start and completion.

Step 4: Human-approval gates (taffy only)

When a stage has gate: human-approval:

  1. Complete stage work (produce artifact).
    • For design review stages (wireframe, visual-identity, design-tokens, ui-mockup-builder, design-refresh), artifact production is mandatory:
      • create at least one previewable artifact (.excalidraw, .html, .svg, .png, .jpg, .jpeg, .webp, or .md) under docs/design (or approved artifact dirs), and
      • the required formats depend on design.target (project.config.yaml, default web): web = .md + .html + .excalidraw; tui = .md + .excalidraw (no .html). A run that produces fewer than the required formats is incomplete, and
      • update .claude/state/design-artifacts.json with current stage artifact paths so the review portal can update live.
    • Path + completeness gate for wireframe stage (mandatory before PAUSING):
      # Verify wireframes are in the canonical location and the required formats exist.
      # Required formats depend on design.target (web: md+html+excalidraw; tui: md+excalidraw).
      TARGET=$(sed -n 's/^[[:space:]]*target:[[:space:]]*\([a-z]*\).*/\1/p' project.config.yaml 2>/dev/null | head -1)
      [ -z "$TARGET" ] && TARGET=web
      CANONICAL=$(find docs/design/wireframes -name "*.wireframe.*" 2>/dev/null | wc -l | tr -d ' ')
      MISPLACED=$(find docs -name "*.wireframe.*" -not -path "*/docs/design/wireframes/*" 2>/dev/null)
      MISSING_HTML=""
      if [ "$TARGET" != "tui" ]; then
        MISSING_HTML=$(find docs/design/wireframes -name "*.wireframe.md" 2>/dev/null | while read md; do
          base="${md%.wireframe.md}"
          [ ! -f "${base}.wireframe.html" ] && echo "${base}.wireframe.html"
        done)
      fi
      MISSING_EXCALIDRAW=$(find docs/design/wireframes -name "*.wireframe.md" 2>/dev/null | while read md; do
        base="${md%.wireframe.md}"
        [ ! -f "${base}.wireframe.excalidraw" ] && echo "${base}.wireframe.excalidraw"
      done)
      if [ "$CANONICAL" -eq 0 ]; then
        if [ -n "$MISPLACED" ]; then
          echo "PIPELINE GATE FAILED: wireframes found at wrong path(s): $MISPLACED"
          echo "Canonical path is docs/design/wireframes/ — move files there before this stage can complete."
        else
          echo "PIPELINE GATE FAILED: no wireframe artifacts in docs/design/wireframes/"
        fi
        # set maple.json FAILED and stop
      fi
      if [ -n "$MISSING_HTML" ]; then
        echo "PIPELINE GATE FAILED: missing .wireframe.html for: $MISSING_HTML — generate it now."
        # set maple.json FAILED and stop
      fi
      if [ -n "$MISSING_EXCALIDRAW" ]; then
        echo "PIPELINE GATE FAILED: missing .wireframe.excalidraw for: $MISSING_EXCALIDRAW — generate it now."
        # set maple.json FAILED and stop
      fi
      
      If the gate fails, produce the missing files before re-running the gate check.
    • If no reviewable artifact exists for a design gate, set maple.json to FAILED and stop.
  2. Write PAUSED state:
{ "stage": "<name>", "status": "PAUSED", "awaiting_approval": "<name>", "updated_at": "<iso8601>" }
  1. Write stage name to .claude/state/approval-pending.txt.
  2. Output:
TAFFY PAUSED — awaiting human approval
Stage:    <stage-name>
Artifact: <artifact path or description>

Approve via the maple TUI ([P] pipeline → [a] approve) or reply "approved" / "continue".
I will not advance to the next stage until approval is confirmed.
  1. Poll: timeout 540 bash -c 'until [ ! -f .claude/state/approval-pending.txt ]; do sleep 2; done'
    • On timeout (exit 124), re-run the same poll. The Bash tool caps at 10 min per call; re-polling across calls lets approval delays exceed that bound.
    • Also accept an explicit "approved" / "continue" reply in chat.
    • When the user approves via the maple TUI ([P] → [a]), the TUI deletes the pending file and sends a "continue" keystroke to the agent's pane via the active multiplexer (outer tmux/zellij, or a detached tmux new-session wrapper). Either signal is sufficient to resume.
  2. On resume: update to RUNNING, advance to next stage.
  3. While paused, monitor .claude/state/design-feedback.json:
    • status: requested_changes or status: rejected means apply the requested updates before advancing.
    • Treat attachments as required review inputs (uploaded files such as .excalidraw, images, HTML, text), typically under docs/design/review-input/.
    • Summarize how each feedback item and attachment was addressed before continuing.

Step 5: Completion

{ "taffy": "<name>", "stage": "DONE", "status": "DONE", "awaiting_approval": null, "updated_at": "<iso8601>" }

Output:

TAFFY COMPLETE — <name>
Stages run: N
Duration:   <elapsed>

Failure Handling

After 3 consecutive failures on any stage:

{ "stage": "<name>", "status": "FAILED", "error": "<summary>", "updated_at": "<iso8601>" }

Stop. Report failed stage and error to human. Do not proceed.

Session Context

On startup, read .claude/state/sessions.json if it exists:

{ "claude": "<uuid>", "opencode": "<id>", "copilot": "<id>" }

Use the matching session ID when resuming work within an existing agent session.

State File Reference

All state in .claude/state/. TUI and skill share these files.

.claude/state/maple.json

Field Owner Values
taffy skill workflow/skill/agent name
stage skill current stage name
status skill RUNNING, PAUSED, DONE, FAILED
awaiting_approval skill local MAPLE stage name (e.g., spec-kit = Specification Knowledge & Integration Toolkit) or null — not an external package reference
pipeline skill standard if running 8-phase
started_at skill ISO 8601
updated_at skill ISO 8601
state TUI running or exited
ts TUI ISO 8601

Merge-not-overwrite: read existing file, update only owned fields, re-write.

.claude/state/approval-pending.txt

Skill writes stage name. TUI deletes when user presses [a].

.claude/state/sessions.json

TUI writes harness→session-ID map. Skill reads for session resume.

Skip Conditions

  • spike/* and chore/* branches: skip Spec-Kit stages, run implementation stages.
  • Stage when: ui:true on a ui: false story: skip silently, log [pipeline-runner] SKIP stage=<name> reason=ui:false unless quick-launch override is active (force_ui=true, launch_source=maple-x).
提供Playwright CLI交互探索、基于快照的页面操作及会话管理功能,支持将交互过程转化为测试代码并执行E2E测试与API请求验证。
需要交互式浏览网页或进行手动点击填充操作 需要将浏览器操作记录转换为自动化测试脚本 需要运行或调试现有的Playwright E2E测试套件
.opencode/skills/playwright-cli/SKILL.md
npx skills add kinncj/Heimdall --skill playwright-cli -g -y
SKILL.md
Frontmatter
{
    "name": "playwright-cli",
    "description": "Use Playwright CLI for interactive browser exploration and snapshot-based interaction. Use when writing or running browser automation tests."
}

SKILL: Playwright CLI

When to Use

Use Playwright CLI (playwright-cli) for interactive browser exploration and snapshot-based interaction. Use npx playwright test for running actual test suites.

Installation

npm install -g @playwright/cli
npx playwright install  # installs browsers

Core CLI Commands

Browser Exploration

# Open browser to URL
playwright-cli open http://localhost:3000

# Take accessibility snapshot (YAML with element references)
playwright-cli snapshot
# Output: saves to disk as snapshot.yml — review before acting

# Interact by element reference (e.g., e21 from snapshot)
playwright-cli click e21
playwright-cli fill e35 "test@example.com"
playwright-cli press e35 Enter
playwright-cli screenshot  # saves to disk

Session Management

# Named sessions for auth state
playwright-cli --session=auth open http://localhost:3000/login
playwright-cli --session=auth fill e10 "admin@example.com"
playwright-cli --session=auth fill e11 "password"
playwright-cli --session=auth click e20  # submit button
playwright-cli session-list

Snapshot-to-Test Workflow

  1. playwright-cli open http://localhost:3000
  2. playwright-cli snapshot → review element refs in snapshot.yml
  3. Interact: playwright-cli click e21, playwright-cli fill e35 "value"
  4. Observe results
  5. Convert to test file:
// tests/e2e/feature.spec.ts
import { test, expect } from '@playwright/test';

test('should {outcome} when {action}', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: 'Sign In' }).click();
  await page.getByLabel('Email').fill('test@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Test Execution

# Run all E2E tests
npx playwright test tests/e2e/

# Run specific test file
npx playwright test tests/e2e/auth.spec.ts

# Visual debugger
npx playwright test --ui

# Headed mode (visible browser)
npx playwright test --headed

# Generate tests by recording
npx playwright codegen http://localhost:3000

# View HTML report
npx playwright show-report

API Testing (Playwright request fixture)

test('POST /api/users returns 201', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: {
      email: 'test@example.com',
      name: 'Test User'
    }
  });
  expect(response.status()).toBe(201);
  const body = await response.json();
  expect(body).toHaveProperty('id');
});

Token Efficiency Note

Use playwright-cli snapshot (saves to disk) instead of screenshots in context. The compact YAML element refs (~1,350 tokens) vs accessibility tree injection (~5,700 tokens).

提供PostgreSQL数据库开发的最佳实践,涵盖基于Flyway的迁移命名、带行级安全(RLS)和自动更新时间戳的表结构创建、查询优化(索引与EXPLAIN分析)及PgBouncer连接池配置。遵循幂等SQL原则,禁止修改历史迁移文件。
编写或生成PostgreSQL数据库模式定义 创建数据库迁移脚本 优化复杂SQL查询性能 配置PostgreSQL连接池
.opencode/skills/postgresql-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill postgresql-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "postgresql-patterns",
    "description": "Apply PostgreSQL schema, migration, and query patterns with idempotent SQL. Use when writing database schemas or migrations."
}

SKILL: PostgreSQL Patterns

Migration Naming

migrations/
  V001__create_users.sql          # Flyway
  0001_create_users.sql           # Generic
  20240115_create_users.sql       # Timestamp-based

Table Pattern with RLS

-- Create table
CREATE TABLE IF NOT EXISTS users (
  id          uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  email       text UNIQUE NOT NULL,
  name        text NOT NULL,
  role        text NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
  created_at  timestamptz DEFAULT now() NOT NULL,
  updated_at  timestamptz DEFAULT now() NOT NULL
);

-- Indexes
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
CREATE INDEX CONCURRENTLY idx_users_role ON users(role) WHERE role != 'user';

-- Updated_at trigger
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_updated_at
  BEFORE UPDATE ON users
  FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY users_select_own ON users
  FOR SELECT USING (id = current_user_id());

CREATE POLICY users_admin_all ON users
  FOR ALL USING (current_user_role() = 'admin');

Query Optimization

-- Always EXPLAIN ANALYZE in development
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > now() - interval '30 days'
GROUP BY u.id;

-- Partial index for common filter
CREATE INDEX idx_orders_pending
  ON orders(created_at)
  WHERE status = 'pending';

-- GIN index for full-text search
CREATE INDEX idx_products_search
  ON products USING gin(to_tsvector('english', name || ' ' || description));

Connection Pooling (PgBouncer)

# pgbouncer.ini
[databases]
app = host=127.0.0.1 port=5432 dbname=app

[pgbouncer]
pool_mode = transaction  # transaction pooling for most apps
max_client_conn = 100
default_pool_size = 20

Rules

  • NEVER modify existing migration files.
  • Always create new migration for schema changes.
  • Index all foreign keys.
  • Use CONCURRENTLY for index creation on large tables.
  • EXPLAIN ANALYZE all queries > 100ms.
  • Enable RLS on tables with user data.
提供 Redis 集成最佳实践,涵盖缓存旁路、限流、会话存储等模式。规范键名格式,强调设置 TTL、使用 SCAN 和管道操作,确保高性能与稳定性。
需要实现 Redis 缓存逻辑 设计 API 限流功能 配置会话存储 优化 Redis 性能
.opencode/skills/redis-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill redis-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "redis-patterns",
    "description": "Apply Redis data structure, caching, and pub\/sub patterns. Use when integrating Redis into a service."
}

SKILL: Redis Patterns

Key Design Convention

{app}:{domain}:{entity}:{id}
# Examples:
myapp:user:session:usr_123
myapp:product:cache:prod_456
myapp:rate:api:ip_1.2.3.4
myapp:queue:emails:pending

Cache-Aside Pattern

async function getUser(id: string): Promise<User> {
  const key = `myapp:user:data:${id}`;
  const ttl = 3600; // 1 hour

  // Try cache first
  const cached = await redis.get(key);
  if (cached) {
    return JSON.parse(cached) as User;
  }

  // Cache miss — fetch from DB
  const user = await db.users.findById(id);
  if (!user) throw new Error('User not found');

  // Store in cache with TTL
  await redis.setex(key, ttl, JSON.stringify(user));
  return user;
}

// Invalidate on update
async function updateUser(id: string, data: Partial<User>): Promise<User> {
  const user = await db.users.update(id, data);
  await redis.del(`myapp:user:data:${id}`);
  return user;
}

Rate Limiting with Sorted Set

async function rateLimit(ip: string, limit: number, windowSeconds: number): Promise<boolean> {
  const key = `myapp:rate:api:${ip}`;
  const now = Date.now();
  const windowStart = now - (windowSeconds * 1000);

  const pipeline = redis.pipeline();
  pipeline.zremrangebyscore(key, 0, windowStart);
  pipeline.zadd(key, now, `${now}`);
  pipeline.zcard(key);
  pipeline.expire(key, windowSeconds);

  const results = await pipeline.exec();
  const count = results?.[2]?.[1] as number;
  return count <= limit;
}

Session Store

// Using ioredis with connect-redis
import session from 'express-session';
import RedisStore from 'connect-redis';

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24, // 24 hours
  }
}));

Rules

  • ALWAYS set TTLs on cache keys — never store indefinitely.
  • Use SCAN not KEYS in production (non-blocking).
  • Use pipelining for multiple operations.
  • Key convention: {app}:{domain}:{entity}:{id}.
  • Monitor with: redis-cli info memory and redis-cli monitor (dev only).
  • Set maxmemory-policy allkeys-lru for cache-only Redis instances.
用于撰写和管理架构决策记录(ADR)。当涉及重大技术选型、框架引入或模式变更时,按标准模板在docs/specs/下创建文档,涵盖背景、方案对比、成本及安全影响,以记录决策依据与后续行动。
需要记录重大架构决策 选择非平凡的技术方案 引入新技术或框架 变更显著现有模式
.opencode/skills/rfc-adr/SKILL.md
npx skills add kinncj/Heimdall --skill rfc-adr -g -y
SKILL.md
Frontmatter
{
    "name": "rfc-adr",
    "description": "Author and manage Architecture Decision Records (ADRs) in docs\/specs\/. Use when a significant architectural decision needs to be documented."
}

SKILL: RFC / Architecture Decision Records

ADR Format

Every significant architectural decision gets an ADR. Store in: docs/specs/{feature}/adr.md

# ADR-{N}: {Short Title}

## Status
Proposed | Accepted | Deprecated | Superseded by ADR-{N}

## Context
What is the problem or situation that requires a decision?
What constraints exist?

## Goals
- {Specific, measurable goal}

## Non-goals
- {What this ADR does NOT address}

## Proposal
{Detailed technical proposal. Include code snippets if helpful.}

## Alternatives Considered

### Option A: {Name}
**Description:** {What it is}
**Pros:** {list}
**Cons:** {list}

### Option B: {Name}
**Description:** {What it is}
**Pros:** {list}
**Cons:** {list}

## Trade-offs and Risks
{Analysis of trade-offs. What could go wrong?}

## Impact

### Cost (FinOps)
{Estimated cloud cost impact. Monthly estimate if possible.}

### Operations (SRE)
{New runbook requirements. Alert thresholds. On-call implications.}

### Security
{Changes to attack surface. New threat vectors. Compliance implications.}

### Team
{Skill requirements. Training needed. Hiring implications.}

## Decision
{Final decision and rationale. Who made it and when.}

## Next Steps
- [ ] {Action item with owner}
- [ ] {Action item with owner}

When to Write an ADR

  • Choosing between two or more non-trivial technical approaches.
  • Adopting a new technology or framework.
  • Changing a significant existing pattern.
  • Making a trade-off with known long-term implications.

When NOT to Write an ADR

  • Obvious choices with no real alternatives.
  • Implementation details within an agreed-upon approach.
  • Temporary decisions expected to change soon.
调用橡皮鸭审查员获取独立二方意见,用于计划、复杂代码实现或测试完成后的质量把关。支持PLAN/CODE/TEST三种模式,输出CRITICAL/WARN/INFO列表及APPROVE/REQUEST_CHANGES裁决,辅助发现遗漏问题并决定后续流程。
Phase 3计划完成后 复杂多文件实现后 编写测试用例后运行前 对当前工作感到不确定时
.opencode/skills/rubber-duck/SKILL.md
npx skills add kinncj/Heimdall --skill rubber-duck -g -y
SKILL.md
Frontmatter
{
    "name": "rubber-duck",
    "description": "Invoke the rubber duck reviewer for a second opinion. Use after planning, after complex multi-file implementations, or after writing tests. Produces a focused CRITICAL\/WARN\/INFO list and a APPROVE\/REQUEST_CHANGES verdict."
}

rubber-duck skill

Invoke @rubber-duck to get an independent second opinion on a plan, implementation, or test suite. This is the same pattern as GitHub Copilot CLI's built-in Rubber Duck feature — a different perspective to catch what the primary agent missed.

When to invoke

Checkpoint What to pass What you get back
After Phase 3 (plan complete) plan.md + test-plan.md PLAN REVIEW
After complex multi-file implementation Changed files / diff CODE REVIEW
After tests written, before running Test files TEST REVIEW
Any time you feel uncertain Anything relevant Focused critique

How to call it

@rubber-duck Mode: PLAN REVIEW

Plan: <paste plan.md content or path>
Story: <paste story frontmatter + scenarios>
@rubber-duck Mode: CODE REVIEW

Files changed:
- src/scheduler.ts
- src/jobs/notify.ts
- src/queue/index.ts

<paste diff or file content>
@rubber-duck Mode: TEST REVIEW

Test files:
- tests/unit/scheduler.test.ts
- tests/e2e/notify.spec.ts
- features/scheduler.feature

<paste test content>

Verdict handling

Verdict Orchestrator action
APPROVE Proceed to next phase
REQUEST_CHANGES Send findings to the responsible agent for revision; re-review once

CRITICAL findings always block. WARN findings are surfaced to the human for a decision. INFO findings are logged but do not block.

Tips

  • You do not need to prepare anything special — just pass the relevant content.
  • The rubber duck will not rewrite your code. It only surfaces findings. Acting on them is your job.
  • For Copilot CLI users: the built-in /experimental Rubber Duck activates automatically at the same checkpoints. This skill adds equivalent coverage when using Claude Code or OpenCode.
在提交代码前运行ship-safe扫描,检查密钥、漏洞及风险模式。需启用环境变量后使用,解析结果按严重性分类:关键问题必须修复,警告供参考,确保发布安全。
打开PR前 合并功能分支到主分支前 添加新依赖后 修改认证或基础设施配置后
.opencode/skills/ship-safe/SKILL.md
npx skills add kinncj/Heimdall --skill ship-safe -g -y
SKILL.md
Frontmatter
{
    "name": "ship-safe",
    "description": "Run ship-safe security and quality audit on the current project. Executes npx ship-safe audit . and reports findings by severity. Use before shipping any feature or PR."
}

SKILL: Ship-Safe Audit

What It Does

Runs ship-safe — a pre-ship security and quality scanner that checks for secrets, vulnerabilities, and risky patterns before code reaches production.

Usage

npx ship-safe audit .

Run from the project root. No install required (npx fetches it on demand).

Opt-In

Ship-safe is disabled by default. To enable it:

  • CI/CD: set the repository variable ENABLE_SHIP_SAFE=true in GitHub → Settings → Variables
  • Agents / local: set env var ENABLE_SHIP_SAFE=true before invoking /ship-safe

When to Use

Only run if ENABLE_SHIP_SAFE=true is set. When enabled, appropriate moments are:

  • Before opening a PR
  • After adding new dependencies
  • Before merging any feature branch to main
  • After touching auth, secrets handling, or infra config

Output Interpretation

Symbol / keyword Severity Action
✓ PASS / ok / no issues Clean Safe to ship
⚠ WARN / MEDIUM / LOW Advisory Review before shipping
✗ FAIL / ERROR / CRITICAL / HIGH Blocker Must fix before shipping

Agent Instructions

  1. Run npx ship-safe audit . from the repo root.
  2. Parse stdout for CRITICAL/HIGH findings — these are blockers.
  3. For each blocker: report the file, line, and finding description.
  4. For MEDIUM/LOW: report as advisory, do not block.
  5. If all checks pass: confirm "ship-safe: clean" and proceed.
  6. If blockers found: halt the task, report findings to Orchestrator.

Example Integration (architect / pre-ship checklist)

/ship-safe

The skill runs the audit, colors findings by severity, and surfaces blockers before any merge action.

用于生成符合规范的 Gherkin 验收测试故事文件。在实现前创建 docs/stories/ 下的 .md 文件,包含元数据、用户故事及场景。支持状态机流程、自动化校验脚本及复杂度场景指南,确保需求明确且可执行。
需要为新功能编写规格说明时 请求生成 Gherkin 格式的验收标准时 验证现有故事文件是否已批准或格式正确时
.opencode/skills/spec-kit/SKILL.md
npx skills add kinncj/Heimdall --skill spec-kit -g -y
SKILL.md
Frontmatter
{
    "name": "spec-kit",
    "description": "Write a Gherkin story file to docs\/stories\/ for a feature. The story IS the spec. No intermediate PROBLEM\/SPEC\/PLAN\/TASKS artifacts."
}

SKILL: spec-kit

Purpose

Produce a complete Gherkin story file in docs/stories/ before any implementation begins. One invocation = one story file. The story file is the minimum spec — no additional artifacts required.

State Machine

feature description → story draft → human approval → DISCOVER

No step may begin until the previous one completes. Approval = human sets status: approved in the story frontmatter, or confirms verbally.

Story File Location

docs/stories/{epic-slug}-{feature-slug}.md   # with epic context
docs/stories/{feature-slug}.md               # standalone feature

Story File Template

---
id: "{feature-slug}"
title: "{Feature Title}"
epic: "{epic-slug or null}"
priority: "high|medium|low"
ui: false
adr_required: false
phase: discover
labels:
  - "type:feature"
  - "priority:{priority}"
status: draft
---

## Story

**As a** {user role},
**I want** {what they want},
**so that** {business outcome}.

## Acceptance Criteria

```gherkin
@story:{feature-slug} @priority:{priority}
Feature: {Feature Title}

  Scenario: {happy path title}
    Given {precondition}
    When {action}
    Then {outcome}

  Scenario: {failure or edge case title}
    Given {precondition}
    When {action}
    Then {outcome}

Definition of Done

  • All Gherkin scenarios have passing step implementations
  • Unit tests written and passing
  • Code reviewed and approved
  • Documentation updated if user-facing

## Validate a Story File

```bash
validate_story() {
  local file="$1"

  if [ ! -f "$file" ]; then
    echo "[spec-kit] MISSING  $file"
    return 1
  fi

  if ! grep -qE "^\s*(Scenario|Scenario Outline):" "$file"; then
    echo "[spec-kit] FAIL  $file — no Gherkin scenarios found"
    return 1
  fi

  STATUS=$(grep -m1 "^status:" "$file" | awk '{print $2}' | tr -d '"')
  if [ "$STATUS" != "approved" ]; then
    echo "[spec-kit] BLOCKED  $file  status=$STATUS  required=approved"
    return 1
  fi

  echo "[spec-kit] OK  $file  status=approved"
}

# Example
validate_story "docs/stories/user-auth-reset-password.md" || exit 1

Check All Stories Are Approved

FAIL=0
STORIES=$(find docs/stories -name "*.md" ! -name "_template.md" 2>/dev/null || true)

if [ -z "$STORIES" ]; then
  echo "[spec-kit] SKIP  no story files found"
  exit 0
fi

for story in $STORIES; do
  validate_story "$story" || FAIL=1
done

exit $FAIL

Scenario Count Guidelines

Feature complexity Minimum scenarios
Simple CRUD 2 (happy path + not-found/validation failure)
Auth / permissions 3 (success + unauthorized + invalid input)
Multi-step workflow 1 per step + 1 end-to-end
UI component 2 (renders correctly + interaction)

Do not invent scenarios not implied by the feature description. Mark unknown cases as TODO and flag for human review.

Skip Conditions

BRANCH=$(git branch --show-current)
if echo "$BRANCH" | grep -qE '^(spike|chore)/'; then
  echo "[spec-kit] SKIP  branch=$BRANCH — spike/chore exempt"
  exit 0
fi

Failure Modes

Condition Action
No feature description given Ask the human before writing anything
Story has zero Scenario: blocks Refuse to emit. Require at least one scenario.
Story status: rejected Surface rejection reason. Do not advance.
Duplicate story file exists Confirm with human before overwriting.

Logging

[spec-kit] WRITE   docs/stories/user-auth-reset-password.md
[spec-kit] HALT    awaiting human approval
[spec-kit] OK      docs/stories/user-auth-reset-password.md  status=approved
[spec-kit] SKIP    spike/* branch — spec-kit not required
用于在功能上线前评估运营就绪状态,涵盖故障模式分析、可观测性指标、告警规则及回滚计划。通过标准化检查确保服务稳定性与快速恢复能力。
新功能上线前的生产就绪评审 重大变更发布前的风险评估
.opencode/skills/sre-review/SKILL.md
npx skills add kinncj/Heimdall --skill sre-review -g -y
SKILL.md
Frontmatter
{
    "name": "sre-review",
    "description": "Evaluate operational readiness: SLOs, alerting, runbooks, and rollback plan before launch. Use when reviewing a feature for production readiness."
}

SKILL: SRE Review

Purpose

Evaluate operational readiness of new features before launch.

Failure Mode Analysis

For each new component, document:

  1. What can fail? (service down, latency spike, data corruption, cascade)
  2. Blast radius? (who is affected? how many users?)
  3. Detection time? (how quickly will alerts fire?)
  4. Recovery time? (how long to restore service?)
  5. Can it be rolled back? (feature flag? migration rollback?)

Observability Requirements

Metrics (Prometheus/CloudWatch/Datadog)

{feature}_requests_total{status="success|error"} counter
{feature}_request_duration_seconds histogram
{feature}_active_connections gauge
{feature}_errors_total{type="validation|timeout|upstream"} counter

Logs (Structured JSON)

{
  "timestamp": "ISO8601",
  "level": "info|warn|error",
  "service": "{service-name}",
  "trace_id": "{distributed-trace-id}",
  "user_id": "{anonymized}",
  "action": "{what happened}",
  "duration_ms": 42,
  "result": "success|error",
  "error": "{message if error}"
}

Traces

  • Instrument all cross-service calls with OpenTelemetry.
  • Trace IDs propagated via W3C Trace Context headers.

Alerts

Alert Condition Severity Action
High error rate error_rate > 1% for 5m P1 Page on-call
Slow responses p99 > 2s for 10m P2 Notify team
Saturation CPU > 80% for 15m P2 Scale out

Runbook Template

docs/runbooks/{feature}-runbook.md

# Runbook: {Feature Name}

## Symptoms
- {Alert name}: {what the user sees}

## Diagnosis
1. Check logs: `kubectl logs -l app={service} --tail=100`
2. Check metrics: {dashboard URL}
3. Check dependencies: {health check commands}

## Mitigation
### Option A: Restart service
```bash
kubectl rollout restart deployment/{service}
kubectl rollout status deployment/{service}

Option B: Feature flag off

# Disable via environment variable
kubectl set env deployment/{service} FEATURE_{NAME}_ENABLED=false

Escalation

  • On-call: {PagerDuty rotation}
  • Escalation: {eng manager}
  • War room: {Slack channel}
维护故事Markdown文件与GitHub Issue的双向一致性。同步标题、正文、标签、状态及DoD检查项,支持新建Issue并回填编号,确保文件叙事与Issue元数据准确对应。
需要创建新的GitHub Issue以关联故事文件时 故事文件的Gherkin或验收标准发生更改需同步至Issue时
.opencode/skills/story-issue-sync/SKILL.md
npx skills add kinncj/Heimdall --skill story-issue-sync -g -y
SKILL.md
Frontmatter
{
    "name": "story-issue-sync",
    "description": "Maintain bidirectional consistency between story markdown files and GitHub Issues. Use when syncing story status with GitHub."
}

SKILL: story-issue-sync

Purpose

Maintain bidirectional consistency between story files (docs/stories/*.md) and GitHub Issues. The file is authoritative for narrative and Gherkin. The issue is authoritative for labels, status, project board position, and DoD checklist state.

Ownership Model

Attribute Authoritative source Sync direction
Title Story file (frontmatter title) File → Issue
Body / Gherkin Story file File → Issue (on update)
issue_number Issue (assigned at create) Issue → File
issue_url Issue Issue → File
Labels Issue Issue → File (labels frontmatter)
Status / Phase Issue labels Issue → File (labels frontmatter)
DoD checklist Issue body (task list) Issue → File (on review)
Project board position Project v2 Not persisted to file

Inputs / Outputs

Field Source Notes
story_file docs/stories/*.md Path to the story markdown file
issue_number Issue (post-create) Written back to story frontmatter
issue_node_id gh issue create --json id Passed to gh-projects skill for board add
issue_url Issue (post-create) Written back to story frontmatter

Create: File → Issue

When a new story file exists with issue_number: null:

STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

# Extract frontmatter fields
TITLE=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.search(r'^title:\s*[\"\'](.*?)[\"\']', text, re.MULTILINE)
print(m.group(1) if m else '')
")

LABELS=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.findall(r'^\s+- [\"\'](.*?)[\"\']\s*$', text, re.MULTILINE)
print(','.join(m))
")

MILESTONE=$(python3 -c "
import sys, re
text = open('$STORY_FILE').read()
m = re.search(r'^milestone:\s*[\"\'](.*?)[\"\']', text, re.MULTILINE)
print(m.group(1) if m else '')
")

# Create the issue
RESULT=$(gh issue create \
  --title "$TITLE" \
  --body-file "$STORY_FILE" \
  --label "$LABELS" \
  --milestone "$MILESTONE" \
  --json number,url,id)

ISSUE_NUMBER=$(echo "$RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin)['number'])")
ISSUE_URL=$(echo "$RESULT"    | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")

# Write back to story frontmatter
python3 - <<EOF
import re
text = open('$STORY_FILE').read()
text = re.sub(r'^issue_number: null', f'issue_number: $ISSUE_NUMBER', text, flags=re.MULTILINE)
text = re.sub(r'^issue_url: null', f'issue_url: "$ISSUE_URL"', text, flags=re.MULTILINE)
open('$STORY_FILE', 'w').write(text)
EOF

echo "[story-sync] CREATED  #$ISSUE_NUMBER  ← $STORY_FILE"

Update: File → Issue (body changed)

When story Gherkin or acceptance criteria are edited:

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

gh issue edit "$ISSUE_NUMBER" \
  --body-file "$STORY_FILE"

echo "[story-sync] BODY_UPDATE  #$ISSUE_NUMBER  ← $STORY_FILE"

Sync: Issue → File (labels changed)

When labels on the issue change (e.g., after PO updates priority or phase advances):

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

# Fetch current labels from issue
CURRENT_LABELS=$(gh issue view "$ISSUE_NUMBER" \
  --json labels \
  --jq '[.labels[].name] | join(",")')

# Rewrite labels block in frontmatter
python3 - <<EOF
import re
text = open('$STORY_FILE').read()
label_lines = '\n'.join(f'  - "{l}"' for l in "$CURRENT_LABELS".split(',') if l)
text = re.sub(
    r'^labels:\n(  - .*\n)+',
    f'labels:\n{label_lines}\n',
    text,
    flags=re.MULTILINE
)
open('$STORY_FILE', 'w').write(text)
EOF

echo "[story-sync] LABELS_SYNC  #$ISSUE_NUMBER  → $STORY_FILE"

Detect Drift (file vs issue)

Run before any update to check whether file and issue are in sync:

ISSUE_NUMBER=42
STORY_FILE="docs/stories/user-auth-reset-password-20250416143000-0001.md"

FILE_TITLE=$(python3 -c "
import re
m = re.search(r'^title:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else '')
")

ISSUE_TITLE=$(gh issue view "$ISSUE_NUMBER" --json title --jq '.title')

if [ "$FILE_TITLE" != "$ISSUE_TITLE" ]; then
  echo "[story-sync] DRIFT  title mismatch: file='$FILE_TITLE'  issue='$ISSUE_TITLE'"
  # File is authoritative for title — update the issue
  gh issue edit "$ISSUE_NUMBER" --title "$FILE_TITLE"
fi

Batch Sync (all stories)

find docs/stories -name "*.md" ! -name "_template.md" | while read -r f; do
  NUMBER=$(python3 -c "
import re
m = re.search(r'^issue_number:\s*(\d+)', open('$f').read(), re.MULTILINE)
print(m.group(1) if m else '')
")
  if [ -z "$NUMBER" ]; then
    echo "[story-sync] NEEDS_CREATE  $f"
  else
    echo "[story-sync] HAS_ISSUE    $f  → #$NUMBER"
  fi
done

Failure Modes

Condition Action
Story has issue_number: null Create the issue. Never skip.
Issue number in file but issue is closed Log warning. Do not reopen automatically — escalate to human.
Title drift detected File wins. Update issue title.
Labels on issue unknown to label set Log: [story-sync] UNKNOWN_LABEL {name}. Do not remove.
Story file missing after issue exists Log warning. Issue is not deleted. Human decides.

Logging

[story-sync] CREATED       #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] BODY_UPDATE   #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] LABELS_SYNC   #42  → docs/stories/user-auth-reset-0001.md
[story-sync] DRIFT         #42  title mismatch — issue updated
[story-sync] SKIP          #42  no changes detected
提供Stripe API集成模式,涵盖Webhook处理、支付会话创建及订阅管理。强调签名验证、幂等性键使用及安全规范,附带本地测试命令与TypeScript代码示例,助力安全高效的支付功能开发。
需要实现Stripe支付流程 配置Stripe Webhook接收器 处理Stripe订阅逻辑 解决支付幂等性问题
.opencode/skills/stripe-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill stripe-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "stripe-patterns",
    "description": "Apply Stripe API integration patterns: webhooks, idempotency keys, and subscription handling. Use when integrating Stripe payments."
}

SKILL: Stripe Patterns

Local Testing Setup

# Listen for webhooks and forward to local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger checkout.session.completed
stripe trigger customer.subscription.created

# Monitor logs
stripe logs tail

Webhook Handler Pattern

// ALWAYS verify webhook signatures — never skip
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get('stripe-signature')!;
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
  } catch (err) {
    return new Response(`Webhook Error: ${err.message}`, { status: 400 });
  }

  // Idempotency: check if event already processed
  // Store event.id in database, skip if duplicate

  switch (event.type) {
    case 'payment_intent.succeeded': {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      // Handle success
      break;
    }
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      // Provision access
      break;
    }
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return new Response(JSON.stringify({ received: true }), { status: 200 });
}

Checkout Session Pattern

const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  payment_method_types: ['card'],
  line_items: [{
    price: priceId,
    quantity: 1,
  }],
  success_url: `${baseUrl}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${baseUrl}/cancel`,
  customer_email: user.email,
  metadata: {
    userId: user.id,  // link back to your user
  },
}, {
  idempotencyKey: `checkout-${user.id}-${priceId}`,  // ALWAYS use idempotency keys
});

Rules

  • ALWAYS verify webhook signatures with constructEvent().
  • ALWAYS use idempotency keys on write operations.
  • NEVER log raw card data or full payment method details.
  • Use restricted API keys with minimum required permissions per service.
  • Test every webhook handler with stripe trigger.
  • Store Stripe customer IDs in your database for lookup.
提供 Supabase 本地开发、数据库迁移及边缘函数的标准模式。涵盖 RLS 安全策略配置、TypeScript 类型生成、Deno 边缘函数实现及最佳实践,确保构建过程规范且安全。
需要配置 Supabase 数据库表结构或 RLS 策略 编写 Supabase 边缘函数逻辑 执行 Supabase 本地开发或部署操作 处理 Supabase 数据迁移
.opencode/skills/supabase-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill supabase-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "supabase-patterns",
    "description": "Apply Supabase schema, RLS policies, and edge function patterns. Use when building on Supabase."
}

SKILL: Supabase Patterns

Local Development Workflow

# Start local Supabase stack
supabase start

# Apply migrations
supabase db push

# Generate TypeScript types
supabase gen types typescript --local > src/types/supabase.ts

# Deploy Edge Functions
supabase functions deploy {function-name}

# Stop
supabase stop

Migration Pattern

-- supabase/migrations/{timestamp}_{description}.sql
-- Up migration (always additive in production)

CREATE TABLE IF NOT EXISTS public.{table_name} (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  created_at timestamptz DEFAULT now() NOT NULL,
  updated_at timestamptz DEFAULT now() NOT NULL
);

-- Enable RLS ALWAYS
ALTER TABLE public.{table_name} ENABLE ROW LEVEL SECURITY;

-- RLS Policies
CREATE POLICY "{table_name}_select_own"
  ON public.{table_name} FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "{table_name}_insert_own"
  ON public.{table_name} FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "{table_name}_update_own"
  ON public.{table_name} FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "{table_name}_delete_own"
  ON public.{table_name} FOR DELETE
  USING (auth.uid() = user_id);

Edge Function Pattern

// supabase/functions/{name}/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req: Request) => {
  const authHeader = req.headers.get('Authorization')
  if (!authHeader) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' }
    })
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    { global: { headers: { Authorization: authHeader } } }
  )

  // Never use service_role key in Edge Functions unless absolutely necessary
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
  }

  // ... handler logic
})

Rules

  • Enable RLS on EVERY table with user data. No exceptions.
  • Never expose service_role key to client code.
  • Always use supabase gen types typescript for type safety.
  • Migrations are append-only (never modify existing migration files).
  • Test with supabase start before deploying.
驱动TDD开发流程,遵循红绿重构循环。先写失败测试(RED),再实现最小代码使其通过(GREEN),最后重构优化(REFACTOR)。支持ATDD模式及集成测试容器生命周期管理,确保高质量交付。
需要实现新功能时 执行测试驱动开发任务时
.opencode/skills/tdd-workflow/SKILL.md
npx skills add kinncj/Heimdall --skill tdd-workflow -g -y
SKILL.md
Frontmatter
{
    "name": "tdd-workflow",
    "description": "Drive development with a red-green-refactor TDD cycle, ensuring tests are written before implementation. Use when implementing any new functionality."
}

SKILL: TDD Workflow

The RED → GREEN → REFACTOR Cycle

RED Phase (QA Agent)

  1. Read the acceptance criterion or task description.
  2. Write a test that will fail because the implementation doesn't exist.
  3. Run the test. It MUST fail with a meaningful error (not a syntax error).
  4. If the test passes immediately → the test is wrong. Rewrite it.
  5. Report: file path, test name, failure message.

GREEN Phase (Specialist Agent)

  1. Read the failing test file at the provided path.
  2. Implement the MINIMUM code to make ONLY that test pass.
  3. Do not implement anything not required by the test.
  4. Run the test. It must pass.
  5. Run the full unit suite to verify no regressions.
  6. If the test fails after 3 attempts → escalate to Orchestrator.

REFACTOR Phase (Specialist Agent)

  1. Look for: duplication, poor naming, long functions, complex conditionals.
  2. Clean up without changing behavior.
  3. Run the test again. It must still pass.
  4. Commit: git commit -m "refactor: {what was cleaned up}"

ATDD Pattern (Acceptance Test-Driven Development)

  1. Write the E2E/acceptance test from the acceptance criterion (Given/When/Then).
  2. Watch it fail (RED).
  3. Drive out unit tests and implementation to make it pass (GREEN).
  4. Acceptance test goes green last.

Integration Test Container Lifecycle

# Before integration tests
docker compose -f docker-compose.test.yml up -d --wait

# Run integration tests
make test-integration

# After tests
docker compose -f docker-compose.test.yml down -v

Rules

  • Test file is created BEFORE implementation file.
  • Test name format: "should {expected outcome} when {condition}".
  • Never weaken an assertion to make a test pass.
  • Never mock what you can test with a real dependency (use containers).
  • Implementation agent receives FILE PATH, not requirement text.
  • Gate: test must fail before implementation, pass after.
指导编写符合最佳实践的Terraform基础设施代码,涵盖模块化目录结构、状态管理及变量模式。规范工作流如初始化、验证和计划,强制成本标注与安全规则,严禁未经指令执行应用。
编写Terraform配置 设计IaC模块结构 管理Terraform状态
.opencode/skills/terraform-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill terraform-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "terraform-patterns",
    "description": "Apply Terraform module structure, state management, and variable patterns for infrastructure as code. Use when writing Terraform."
}

SKILL: Terraform Patterns

Module Structure

infra/terraform/
├── modules/
│   ├── networking/
│   ├── database/
│   └── compute/
└── environments/
    ├── dev/
    │   ├── main.tf
    │   ├── variables.tf
    │   └── terraform.tfvars
    ├── staging/
    └── prod/

Module Pattern

# modules/database/main.tf
resource "aws_db_instance" "main" {
  # finops: ~$180/mo (db.t3.medium, 100GB gp3, single AZ)
  identifier     = "${var.environment}-${var.name}-db"
  engine         = "postgres"
  engine_version = "16"
  instance_class = var.instance_class

  allocated_storage     = var.storage_gb
  storage_type          = "gp3"
  storage_encrypted     = true

  username = var.username
  password = var.password

  vpc_security_group_ids = [aws_security_group.db.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  backup_retention_period = var.environment == "prod" ? 7 : 1
  deletion_protection     = var.environment == "prod"

  tags = merge(var.tags, {
    Environment = var.environment
    ManagedBy   = "terraform"
  })
}

Workflow

# Initialize (first time or after provider changes)
terraform init

# Format check (CI)
terraform fmt -check -recursive

# Validate syntax
terraform validate

# Plan (always before apply)
terraform plan -out=plan.tfplan -var-file=environments/dev/terraform.tfvars

# Apply (requires explicit instruction)
terraform apply plan.tfplan

# State inspection
terraform show
terraform state list

Rules

  • ALWAYS run terraform validate before reporting complete.
  • ALWAYS run terraform plan to show what will change.
  • NEVER run terraform apply without explicit instruction.
  • ALWAYS annotate resources with # finops: cost estimate.
  • Use workspaces or separate state files per environment.
  • Enable state locking (S3 + DynamoDB or Terraform Cloud).
  • Tag all resources with environment, managed-by, and cost-center.
基于STRIDE框架对组件和信任边界进行威胁建模,生成包含资产、风险分析及缓解措施的威胁注册表。适用于功能发布前的安全审查,确保识别并处理Spoofing等六类安全风险。
新功能安全审查 系统架构变更评估 敏感数据处理流程设计
.opencode/skills/threat-modeling/SKILL.md
npx skills add kinncj/Heimdall --skill threat-modeling -g -y
SKILL.md
Frontmatter
{
    "name": "threat-modeling",
    "description": "Perform STRIDE threat modeling for each component and trust boundary and produce a threat register. Use when reviewing a feature for security."
}

SKILL: Threat Modeling (STRIDE)

Output Location

docs/specs/{feature-slug}/threat-model.md

STRIDE Framework

For each component and trust boundary, evaluate all 6 threat categories:

Letter Threat Question
S Spoofing Can an attacker impersonate a user, service, or system?
T Tampering Can data be modified in transit or at rest without detection?
R Repudiation Can a user deny performing an action?
I Information Disclosure Can sensitive data leak to unauthorized parties?
D Denial of Service Can an attacker prevent legitimate users from accessing the service?
E Elevation of Privilege Can an attacker gain capabilities beyond what is authorized?

Threat Model Template

# Threat Model: {Feature Name}

## Assets
| Asset | Sensitivity | Owner |
|-------|-------------|-------|
| User PII | High | {team} |
| Payment data | Critical | {team} |
| API keys | Critical | {team} |

## Trust Boundaries
- Internet <-> Load Balancer
- Load Balancer <-> Application
- Application <-> Database
- Application <-> Third-party APIs

## STRIDE Analysis

### {Component Name}

**S — Spoofing**
- Risk: {description}
- Likelihood: High/Medium/Low
- Impact: High/Medium/Low
- Mitigation: {control}

**T — Tampering**
...

**R — Repudiation**
...

**I — Information Disclosure**
...

**D — Denial of Service**
...

**E — Elevation of Privilege**
...

## Risk Register
| Threat | Component | Likelihood | Impact | Risk Level | Mitigation | Status |
|--------|-----------|------------|--------|------------|------------|--------|
| SQL Injection | API | High | Critical | Critical | Parameterized queries | Mitigated |

## Mitigations Required Before Launch
- [ ] {Critical mitigation 1}
- [ ] {Critical mitigation 2}

Common Mitigations

  • Authentication: JWT with short expiry, refresh token rotation.
  • Authorization: RBAC, RLS on database.
  • Input validation: Zod/FluentValidation on all inputs.
  • Rate limiting: Per-IP and per-user limits.
  • Encryption: TLS 1.3 in transit, AES-256 at rest.
  • Audit logging: All write operations logged with actor + timestamp.
  • Secrets management: Never in code; use environment variables or vault.
提供Vercel部署、Edge函数及环境变量配置的最佳实践。涵盖vercel.json设置、运行时选择、中间件模式、安全头配置及CLI部署流程,旨在优化Vercel平台上的应用开发与发布效率。
需要配置Vercel部署文件 选择Edge或Node.js运行时 管理Vercel环境变量 编写Next.js中间件逻辑 执行Vercel构建与部署操作
.opencode/skills/vercel-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill vercel-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "vercel-patterns",
    "description": "Apply Vercel deployment, edge function, and environment variable patterns. Use when deploying to Vercel."
}

SKILL: Vercel Patterns

vercel.json Configuration

{
  "framework": "nextjs",
  "buildCommand": "npm run build",
  "devCommand": "npm run dev",
  "installCommand": "npm ci",
  "regions": ["iad1"],
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Strict-Transport-Security", "value": "max-age=31536000" }
      ]
    }
  ],
  "rewrites": [],
  "redirects": [
    {
      "source": "/old-path",
      "destination": "/new-path",
      "permanent": true
    }
  ]
}

Runtime Selection

// Edge Runtime (low-latency, streaming, middleware)
export const runtime = 'edge';

// Node.js Runtime (heavy compute, native modules, file system)
export const runtime = 'nodejs';

// ISR (static with revalidation)
export const revalidate = 3600; // seconds

Environment Variables

# Pull env vars for local development
vercel env pull .env.local

# Add env var
vercel env add SECRET_KEY production

# List env vars
vercel env ls

Deployment Workflow

# Build locally first (catch errors before deploy)
vercel build

# Deploy prebuilt (faster, recommended for CI)
vercel deploy --prebuilt

# Deploy to production
vercel deploy --prod --prebuilt

Middleware Pattern

// middleware.ts (runs at edge, before every request)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Auth check, rate limiting, A/B testing, etc.
  const token = request.cookies.get('token');
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Rules

  • Run vercel build locally before deploying to catch errors.
  • Use vercel env pull to sync environment variables locally.
  • Prefer Edge runtime for middleware and simple API routes.
  • Use Node.js runtime for heavy compute or native module requirements.
  • Set security headers for all API routes.
根据品牌简报生成视觉识别系统(配色、排版、间距),输出JSON供design-tokens使用。支持Web和终端(TUI)目标,确保WCAG对比度合规,需人工审批后生效。
建立品牌视觉识别 从简报生成设计令牌
.opencode/skills/visual-identity/SKILL.md
npx skills add kinncj/Heimdall --skill visual-identity -g -y
SKILL.md
Frontmatter
{
    "name": "visual-identity",
    "description": "Produce a coherent visual identity (palette, typography, spacing, radius) from a brief and serialise to JSON for the design-tokens skill. Use when establishing brand identity."
}

SKILL: visual-identity

Purpose

Produce a coherent visual identity from a brief or set of brand keywords. Outputs are palette, typography pairing, spacing scale, and radius scale — all serialised to JSON files that feed the design-tokens skill. Human approval required before tokens propagate to UI.

Inputs

Field Source Example
brief free-text brand description "modern fintech, trustworthy, minimal"
keywords 3–6 brand keywords ["trust", "clarity", "speed"]
primary_color optional hex override "#2563eb"
existing_tokens path to existing tokens.json docs/design/identity/tokens.json

Outputs

File Location Description
palette.json docs/design/identity/ All color roles with hex + WCAG contrast
typography.json docs/design/identity/ Font families, scale, weights, line-heights
tokens.json docs/design/identity/ W3C DTCG format — canonical token file

Target awareness

When design.target (project.config.yaml, default web) is tui, keep the same JSON outputs but constrain choices to the terminal: colors must be expressible in ANSI-256 (record the nearest 256 index alongside each hex), every fg/bg role pair must clear WCAG 2.2 AA contrast, and typography.json describes terminal text styles (bold, dim/faint, italic, underline, reverse) rather than font families and px sizes. The palette/typography/scale templates below still apply structurally; adapt their values for the terminal.

Palette Structure

docs/design/identity/palette.json:

{
  "brand": {
    "primary":   { "value": "#2563eb", "on": "#ffffff", "contrast_aa": true },
    "secondary": { "value": "#7c3aed", "on": "#ffffff", "contrast_aa": true },
    "accent":    { "value": "#0ea5e9", "on": "#ffffff", "contrast_aa": true }
  },
  "semantic": {
    "success":  { "value": "#16a34a", "on": "#ffffff", "contrast_aa": true },
    "warning":  { "value": "#d97706", "on": "#000000", "contrast_aa": true },
    "error":    { "value": "#dc2626", "on": "#ffffff", "contrast_aa": true },
    "info":     { "value": "#0284c7", "on": "#ffffff", "contrast_aa": true }
  },
  "neutral": {
    "50":  "#f8fafc",
    "100": "#f1f5f9",
    "200": "#e2e8f0",
    "300": "#cbd5e1",
    "400": "#94a3b8",
    "500": "#64748b",
    "600": "#475569",
    "700": "#334155",
    "800": "#1e293b",
    "900": "#0f172a"
  },
  "surface": {
    "background": "#ffffff",
    "foreground": "#0f172a",
    "muted":      "#f1f5f9",
    "border":     "#e2e8f0"
  }
}

WCAG Contrast Check

All palette entries with on values must pass WCAG 2.2 AA (4.5:1 for normal text, 3:1 for large text):

def relative_luminance(hex_color):
    r, g, b = (int(hex_color[i:i+2], 16) / 255 for i in (1, 3, 5))
    def linearize(c):
        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    r, g, b = linearize(r), linearize(g), linearize(b)
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def contrast_ratio(fg, bg):
    l1 = relative_luminance(fg)
    l2 = relative_luminance(bg)
    lighter, darker = max(l1, l2), min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)

# AA normal text requires >= 4.5
# AA large text requires >= 3.0
ratio = contrast_ratio("#2563eb", "#ffffff")
assert ratio >= 4.5, f"FAIL: contrast {ratio:.2f} < 4.5 for #2563eb on #ffffff"

If any pair fails AA, adjust the shade until it passes. Never ship a palette with failing contrast.

Typography Structure

docs/design/identity/typography.json:

{
  "families": {
    "sans":  "Inter, system-ui, -apple-system, sans-serif",
    "mono":  "JetBrains Mono, 'Fira Code', monospace",
    "serif": null
  },
  "scale": {
    "xs":   { "size": "0.75rem",  "line": "1rem" },
    "sm":   { "size": "0.875rem", "line": "1.25rem" },
    "base": { "size": "1rem",     "line": "1.5rem" },
    "lg":   { "size": "1.125rem", "line": "1.75rem" },
    "xl":   { "size": "1.25rem",  "line": "1.75rem" },
    "2xl":  { "size": "1.5rem",   "line": "2rem" },
    "3xl":  { "size": "1.875rem", "line": "2.25rem" },
    "4xl":  { "size": "2.25rem",  "line": "2.5rem" }
  },
  "weights": {
    "regular": 400,
    "medium":  500,
    "semibold": 600,
    "bold":    700
  }
}

Spacing + Radius Scales

Written directly into tokens.json (see design-tokens skill for full schema):

{
  "spacing": {
    "1":  "0.25rem",
    "2":  "0.5rem",
    "3":  "0.75rem",
    "4":  "1rem",
    "6":  "1.5rem",
    "8":  "2rem",
    "12": "3rem",
    "16": "4rem"
  },
  "radius": {
    "none": "0",
    "sm":   "0.125rem",
    "md":   "0.375rem",
    "lg":   "0.5rem",
    "xl":   "0.75rem",
    "full": "9999px"
  }
}

Write palette.json

mkdir -p docs/design/identity
# Write palette.json from the template above, populated with chosen values
# Then validate all contrast ratios before writing
python3 scripts/validate-palette.py docs/design/identity/palette.json

Approval Gate

Mark docs/design/identity/palette.json approved before design-tokens runs:

Add to the top of palette.json:

{ "_meta": { "status": "draft", "approved_by": null, "approved_at": null }, ... }

Check in design-tokens skill:

STATUS=$(python3 -c "import json; print(json.load(open('docs/design/identity/palette.json'))['_meta']['status'])")
[ "$STATUS" = "approved" ] || { echo "BLOCKED: palette not approved"; exit 1; }

Failure Modes

Condition Action
Contrast ratio fails AA Adjust shade. Re-check. Never skip.
existing_tokens provided Merge: only fill missing roles; do not overwrite approved values.
No brief or keywords provided Generate a neutral/professional default palette. Log DEFAULT_PALETTE.
palette.json exists and is approved Do not overwrite. Log SKIP — approved identity exists.

Logging

[visual-identity] PALETTE    docs/design/identity/palette.json  contrast=all-pass
[visual-identity] TYPOGRAPHY docs/design/identity/typography.json
[visual-identity] SKIP       docs/design/identity/palette.json  (approved — locked)
[visual-identity] CONTRAST_FAIL  #2563eb on #ffffff  ratio=3.8  required=4.5
根据用户故事文件生成低保真线框图,支持ASCII、SVG或HTML格式。自动适配Web和TUI目标,确保输出完整并需人工审批后方可进入下游设计阶段。
需要为UI故事创建线框图 从用户故事文件生成低保真原型
.opencode/skills/wireframe/SKILL.md
npx skills add kinncj/Heimdall --skill wireframe -g -y
SKILL.md
Frontmatter
{
    "name": "wireframe",
    "description": "Generate low-fidelity wireframes (ASCII, SVG, or HTML) from user story files. Use when creating wireframes for UI stories."
}

SKILL: wireframe

Purpose

Generate low-fidelity wireframes from user story files. Output is deterministic — given the same story and layout hints, the same wireframe structure is produced. Three output formats: ASCII (default), SVG, HTML. Human approval is required before the wireframe feeds downstream mockup work.

Inputs

Field Source Example
story_file path to story markdown docs/stories/auth-reset-0001.md
ui_components derived from story Gherkin form, button, error message
stack project.config.yaml react-mantine

Outputs

The required files depend on design.target in project.config.yaml (default web) — see Target awareness:

File Location Targets
<story-id>.wireframe.md docs/design/wireframes/ web + tui — ASCII layout + approval metadata
<story-id>.wireframe.excalidraw docs/design/wireframes/ web + tui — editable Excalidraw diagram
<story-id>.wireframe.html docs/design/wireframes/ web only — browser-previewable static wireframe

A wireframe stage that produces fewer than the files required for the active target is incomplete. Do not PAUSE or mark DONE without them.

Target awareness

design.target decides which files are required:

  • web<id>.wireframe.md (ASCII) + .html (preview) + .excalidraw.
  • tui<id>.wireframe.md (ASCII/box-drawing) + .excalidraw. No .html.

Use box-drawing primitives for tui layouts, label panes/overlays/status bar, and include the keybinding legend and focus order inline in the .md. The Excalidraw generator below applies to both targets; the HTML generator is web only.

ASCII Wireframe Primitives

Use these consistently across all wireframes:

┌─────────────────────────────────┐   ← container / card
│  [Label]  [Input Field      ]   │   ← label + text input
│  [Button: Primary Action    ]   │   ← primary button
│  [Button: Secondary]            │   ← secondary button
│  ○ Option A  ○ Option B         │   ← radio group
│  ☐ Checkbox label               │   ← checkbox
│  ▼ Dropdown / Select            │   ← select / combobox
│  ──────────────────────         │   ← divider
│  ⚠ Error message text           │   ← validation error
│  ✓ Success confirmation         │   ← success state
└─────────────────────────────────┘

[Nav: Logo | Item 1 | Item 2 | CTA]  ← navigation bar
[ Sidebar  ][     Main Content    ]  ← two-column layout
[  Col 1  ][  Col 2  ][  Col 3  ]   ← three-column grid
[         Full-width Banner         ]← hero / header band

ASCII Wireframe — Example (Password Reset)

┌──────────────────────────────────────┐
│            Reset Password            │
│                                      │
│  Email                               │
│  [                              ]    │
│                                      │
│  ⚠ No account found for this email  │  ← error state
│                                      │
│  [Button: Send Reset Link       ]    │
│                                      │
│  ← Back to Login                     │
└──────────────────────────────────────┘

Generate Wireframe Files

For each story, produce all three output files. Run these steps:

STORY_FILE="docs/stories/auth-reset-password-20250416143000-0001.md"
STORY_ID=$(python3 -c "
import re
m = re.search(r'^id:\s*[\"\'](.*?)[\"\']', open('$STORY_FILE').read(), re.MULTILINE)
print(m.group(1) if m else 'unknown')
")
mkdir -p docs/design/wireframes

Step 1 — Write ${STORY_ID}.wireframe.md (ASCII layout + approval metadata):

---
story_id: "{story_id}"
story_file: "{story_file}"
status: draft          # draft | approved | rejected
approved_by: null
approved_at: null
---

## Wireframe: {story title}

### Default state
{ASCII wireframe}

### Error state
{ASCII wireframe — validation error}

### Success state
{ASCII wireframe — confirmation}

### Interaction Notes

- Tab order: {list of focusable elements in tab sequence}
- Primary action: {describe}
- Error handling: {describe visible error states}

### Approval

- [ ] Approved by product owner
- [ ] Approved by UX lead (if applicable)

Step 2 — Write ${STORY_ID}.wireframe.html (see HTML template below)

Step 3 — Write ${STORY_ID}.wireframe.excalidraw (see Excalidraw template below)

After writing the files required for the active target, verify they exist (web: md, html, excalidraw; tui: md, excalidraw):

ls docs/design/wireframes/${STORY_ID}.wireframe.md
ls docs/design/wireframes/${STORY_ID}.wireframe.excalidraw
# web target also:
ls docs/design/wireframes/${STORY_ID}.wireframe.html 2>/dev/null || true

If any required file is missing, produce it before continuing.

HTML Wireframe (web target only)

For web targets, always generate the HTML wireframe. Skip this section entirely for tui. Use multiple <section> blocks for multi-state wireframes (default, loading, error, success, empty).

cat > "docs/design/wireframes/${STORY_ID}.wireframe.html" <<'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Wireframe: {story title}</title>
  <style>
    * { box-sizing: border-box; font-family: monospace; }
    body { background: #e9ecef; padding: 2rem; }
    h1 { font-size: 1rem; color: #495057; margin-bottom: 1.5rem; }
    .states { display: flex; flex-wrap: wrap; gap: 1.5rem; }
    .state { background: #f8f9fa; border: 1px solid #adb5bd; border-radius: 4px; padding: 0; min-width: 360px; }
    .state-label { background: #343a40; color: #fff; font-size: .75rem; padding: .25rem .75rem; border-radius: 4px 4px 0 0; }
    .frame { padding: 1.5rem; }
    .screen-title { font-weight: bold; font-size: 1.1rem; margin-bottom: 1rem; border-bottom: 1px solid #dee2e6; padding-bottom: .5rem; }
    label { display: block; font-size: .8rem; color: #495057; margin-bottom: .2rem; margin-top: .75rem; }
    .input { border: 1px solid #868e96; padding: .4rem .6rem; width: 100%; background: #fff; }
    .btn { border: none; padding: .5rem 1rem; cursor: default; margin-top: .75rem; width: 100%; font-weight: bold; }
    .btn-primary { background: #343a40; color: #fff; }
    .btn-secondary { background: transparent; border: 1px solid #343a40; color: #343a40; }
    .error { color: #c0392b; font-size: .8rem; margin-top: .25rem; }
    .success { color: #2d6a4f; font-size: .8rem; margin-top: .25rem; }
    .link { color: #1971c2; font-size: .85rem; margin-top: .75rem; display: block; }
    .nav { display: flex; gap: 1rem; background: #343a40; color: #fff; padding: .6rem 1rem; font-size: .85rem; margin-bottom: .75rem; }
    .badge { background: #868e96; color: #fff; font-size: .7rem; padding: .1rem .4rem; border-radius: 3px; }
    .divider { border: none; border-top: 1px solid #dee2e6; margin: .75rem 0; }
    .tab-order { font-size: .7rem; color: #868e96; margin-top: 1.5rem; border-top: 1px dashed #dee2e6; padding-top: .5rem; }
  </style>
</head>
<body>
  <h1>Wireframe: {story title} — {story_id}</h1>
  <div class="states">

    <div class="state">
      <div class="state-label">Default state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <!-- Add form fields, buttons, content blocks here -->
        <label>Field label</label>
        <input class="input" type="text" placeholder="placeholder" disabled />
        <div class="btn btn-primary">Primary Action</div>
        <a class="link" href="#">Secondary link</a>
        <div class="tab-order">Tab order: Field → Primary Action → Secondary link</div>
      </div>
    </div>

    <div class="state">
      <div class="state-label">Error state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <label>Field label</label>
        <input class="input" type="text" placeholder="placeholder" disabled style="border-color:#c0392b" />
        <div class="error">⚠ Error message describing the problem</div>
        <div class="btn btn-primary">Primary Action</div>
        <div class="tab-order">Tab order: Field → Primary Action</div>
      </div>
    </div>

    <div class="state">
      <div class="state-label">Success state</div>
      <div class="frame">
        <div class="screen-title">{Screen Title}</div>
        <div class="success">✓ Success confirmation message</div>
        <div class="btn btn-secondary">Back / Next step</div>
      </div>
    </div>

  </div>
</body>
</html>
HTMLEOF

Extend with real field names, content, and states from the story Gherkin. One <div class="state"> block per Gherkin scenario.

Excalidraw Wireframe (always required)

Always generate the Excalidraw file — every story, every run. Excalidraw is the canonical editable wireframe format reviewers annotate.

Write the file as valid JSON to docs/design/wireframes/${STORY_ID}.wireframe.excalidraw. Each UI element is one entry in the elements array. Use the element templates below, copy and adapt:

python3 - <<'PYEOF'
import json, pathlib, os

story_id = os.environ.get("STORY_ID", "unknown")
out = pathlib.Path(f"docs/design/wireframes/{story_id}.wireframe.excalidraw")
out.parent.mkdir(parents=True, exist_ok=True)

# ── Element helpers ──────────────────────────────────────────────────────────
def rect(id, x, y, w, h, label="", bg="transparent", stroke="#343a40", bold=False):
    els = [{
        "id": id, "type": "rectangle", "x": x, "y": y, "width": w, "height": h,
        "angle": 0, "strokeColor": stroke, "backgroundColor": bg,
        "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid",
        "roughness": 1, "opacity": 100, "groupIds": [], "roundness": {"type": 3},
        "version": 1, "versionNonce": 1, "isDeleted": False,
        "boundElements": None, "updated": 1, "link": None, "locked": False,
    }]
    if label:
        els.append(text(id + "_lbl", x + w/2, y + h/2, label, bold=bold, anchor="center"))
    return els

def text(id, x, y, content, bold=False, anchor="left", color="#343a40"):
    return {
        "id": id, "type": "text", "x": x, "y": y,
        "width": len(content) * 8, "height": 20,
        "angle": 0, "strokeColor": color, "backgroundColor": "transparent",
        "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid",
        "roughness": 1, "opacity": 100, "groupIds": [], "roundness": None,
        "version": 1, "versionNonce": 1, "isDeleted": False,
        "boundElements": None, "updated": 1, "link": None, "locked": False,
        "text": content, "fontSize": 16,
        "fontFamily": 3,  # monospace
        "textAlign": anchor, "verticalAlign": "middle",
        "baseline": 14, "containerId": None, "originalText": content,
        "lineHeight": 1.25,
        "fontWeight": "bold" if bold else "normal",
    }

def input_field(id, x, y, w, label_text):
    return (
        [text(id + "_lbl", x, y - 18, label_text)] +
        rect(id, x, y, w, 32, stroke="#868e96")
    )

def button_primary(id, x, y, w, label_text):
    return rect(id, x, y, w, 36, label=label_text, bg="#343a40", stroke="#343a40", bold=True)

def button_secondary(id, x, y, w, label_text):
    return rect(id, x, y, w, 36, label=label_text, bg="transparent", stroke="#343a40")

def section_label(id, x, y, content):
    return [text(id, x, y, f"[ {content} ]", color="#868e96")]

# ── Build elements ────────────────────────────────────────────────────────────
elements = []

# Outer frame
elements += rect("frame", 50, 30, 700, 600, stroke="#343a40")

# Screen title
elements.append(text("title", 70, 50, "{Screen Title}", bold=True))

# --- Default state ---
elements += section_label("s_default", 70, 90, "Default state")
elements += input_field("field1", 70, 130, 560, "Field label")
elements += button_primary("btn_primary", 70, 190, 560, "Primary Action")
elements.append(text("link1", 70, 238, "← Secondary link / back", color="#1971c2"))

# --- Error state ---
elements += section_label("s_error", 70, 280, "Error state")
elements += input_field("field1_err", 70, 320, 560, "Field label")
elements += rect("err_border", 70, 320, 560, 32, stroke="#c0392b")
elements.append(text("err_msg", 70, 360, "⚠ Error message text", color="#c0392b"))
elements += button_primary("btn_primary_err", 70, 390, 560, "Primary Action")

# --- Success state ---
elements += section_label("s_success", 70, 450, "Success state")
elements.append(text("success_msg", 70, 490, "✓ Success confirmation", color="#2d6a4f"))
elements += button_secondary("btn_back", 70, 520, 260, "Back / Next step")

doc = {
    "type": "excalidraw",
    "version": 2,
    "source": "MAPLE wireframe-architect",
    "elements": elements,
    "appState": {"viewBackgroundColor": "#f8f9fa", "gridSize": None},
    "files": {},
}
out.write_text(json.dumps(doc, indent=2) + "\n")
print(f"[wireframe] wrote {out}")
PYEOF

Adapt element positions and labels to match the actual story screens. Add more rect/text/input_field/button_primary calls per Gherkin scenario. Do not leave placeholder text ({Screen Title}) in the final file.

Approval Gate

All three files must exist and the .md must be status: approved before mockup or ui-mockup-builder proceeds:

python3 - <<'EOF'
import re, sys, pathlib
sid = open(".claude/state/maple.json").read()  # or pass STORY_ID
# Check required artifacts exist (html is web-only)
cfg = open("project.config.yaml").read() if pathlib.Path("project.config.yaml").exists() else ""
tm = re.search(r'^\s*target:\s*(\w+)', cfg, re.MULTILINE)
target = tm.group(1) if tm else "web"
exts = ["md", "excalidraw"] if target == "tui" else ["md", "html", "excalidraw"]
for ext in exts:
    p = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.{ext}")
    if not p.exists():
        print(f"BLOCKED: missing {p}")
        sys.exit(1)
# Check approval status in .md
md = pathlib.Path(f"docs/design/wireframes/{sid}.wireframe.md").read_text()
m = re.search(r'^status:\s*(\w+)', md, re.MULTILINE)
status = m.group(1) if m else 'draft'
if status != 'approved':
    print(f"BLOCKED: wireframe {sid} not approved (status={status})")
    sys.exit(1)
print("approved — all three artifacts present")
EOF

Failure Modes

Condition Action
Story has no Gherkin Generate skeleton wireframe with placeholder states. Log NO_GHERKIN — skeleton only.
docs/design/wireframes/ missing Create it.
Wireframe .md exists and is approved Do not overwrite. Log SKIP — approved wireframe exists.
Wireframe .md exists and is draft Overwrite only if story Gherkin has changed.
.html or .excalidraw missing despite .md existing Generate the missing file(s) immediately.

Logging

[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.md
[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.html
[wireframe] CREATE   docs/design/wireframes/auth-reset-0001.wireframe.excalidraw
[wireframe] SKIP     docs/design/wireframes/auth-reset-0001.wireframe.md  (approved — locked)
[wireframe] BLOCKED  auth-reset-0001  status=draft — needs approval before mockup
[wireframe] BLOCKED  auth-reset-0001  missing .html — generating now

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-09 13:19
浙ICP备14020137号-1 $Гость$