Agent Skills › kinncj/Heimdall

kinncj/Heimdall

GitHub

对生成的UI执行WCAG 2.2 AA级无障碍审计。支持Web和TUI目标,自动检测axe-core或pa11y工具,检查对比度、键盘交互等合规性,并将结果输出为JSON或PR评论,阻断AA违规合并。

105 skills 55

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级无障碍审计。支持Web和TUI目标,自动检测axe-core或pa11y工具,检查对比度、键盘交互等合规性,并将结果输出为JSON或PR评论,阻断AA违规合并。
故事包含 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
根据设计令牌、故事ID和模拟文件,自动生成完整的UI组件骨架。包含实现文件、Storybook故事、单元测试及Gherkin规范,支持自动检测React或HTML技术栈,生成可运行的TODO占位符代码。
需要创建新的UI组件 基于设计规范生成组件骨架 初始化Storybook故事文件
.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、Python和Java等主流Cucumber框架。
将故事文档转换为自动化测试用例 同步需求文档与测试代码 为新功能生成Cucumber步骤定义骨架
.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 格式的设计令牌文件,并生成 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规则。指导编写安全高效的容器化应用,强调非root运行、缓存优化及生产环境镜像规范。
编写或审查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 全生命周期,支持创建、查看、编辑标签与指派、评论、关闭及列表查询。通过标准化命令和模板实现故事从创建到验收的无干预流转。
需要创建新的 GitHub Issue 需要更新或查看现有 Issue 状态 需要在 Issue 中添加阶段性评论 需要关闭已完成验证的 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 仓库的标签与里程碑。通过脚本确保标签存在或更新属性,支持阶段、类型、优先级及设计等多组标签定义,适用于新项目启动和 CI/CD 流程,仅增改不删除。
初始化新项目时设置标准标签体系 在 CI/CD 流水线中同步标签状态 需要统一团队工作分类(如 bug、feature)时
.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。
用户要求将 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故事文件 更新现有的用户故事文档 生成符合特定命名约定的文件名
.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 (gh) 的完整参考指南,涵盖 Issue 创建、查看、更新、评论及关闭,以及 PR 的草稿创建、审查请求、CI 状态检查和合并操作。
需要管理 GitHub Issues 或 Pull Requests 执行 GitHub 仓库的自动化工作流
.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生成痕迹,使文档、注释和代码提交信息更自然。支持通过命令或对话调用,提供风格校准功能,并检测29种常见AI写作模式以提升内容真实感。
用户要求将文本改写得更像人类写作 在代码合并前检查或优化提交信息和注释 使用/humanizer命令处理粘贴的文本
.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结构或流程时 配置自动化执行或版本控制时
.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部署清单 使用Kustomize管理环境覆盖 配置PodDisruptionBudget 设置HorizontalPodAutoscaler 执行K8s资源验证
.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、部署及状态机图。需遵循节点数限制并确保语法正确,适用于系统架构文档编写。
需要绘制系统架构图时 记录功能模块交互逻辑时 设计数据库实体关系时 描述应用部署拓扑时 梳理业务状态流转时
.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
基于已审批线框图和Design Token,为指定UI技术栈生成高保真组件Mockup代码。支持Web和Tui目标,含预检逻辑与模板。
需要实现已批准的线框图 根据设计令牌生成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注册表自动安装缺失技能,并在执行前强制实施各类Harness指令文件及Gherkin交付范围限制,状态实时同步至maple.json。
用户请求运行特定名称的工作流、技能或智能体 需要跨项目统一调度复杂任务流程 引用了<maple-gherkin-handoff>块进行严格范围控制
.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测试场景,旨在优化Token使用效率。
需要手动探索或调试Web页面 通过截图/快照与网页元素进行交互 需要将手动操作录制为自动化测试脚本 运行或调试Playwright E2E测试套件 需要进行API接口测试
.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安全策略、索引优化、连接池配置及严格的操作规则,适用于编写幂等SQL和数据库架构设计。
编写或审查PostgreSQL数据库表结构 创建或管理数据库迁移文件 优化SQL查询性能与索引策略 配置PgBouncer连接池
.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集成最佳实践,涵盖缓存旁路、基于有序集合的限流及会话存储模式。规范键命名约定,强调设置TTL、使用SCAN和管道操作,确保高性能与稳定性。
需要实现Redis缓存策略 实施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)。定义标准模板,涵盖背景、目标、提案、替代方案及多维度影响分析。明确适用场景如技术选型或重大模式变更,以及不适用场景,确保重要决策有据可查。
需要记录重大架构决策 在多种技术方案中进行选择 引入新技术或框架 改变现有显著设计模式
.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.
提供独立第二意见,用于计划、代码和测试审查。在复杂实现后或感到不确定时触发,输出关键/警告/信息列表及批准或请求变更的裁决,辅助决策与迭代。
Phase 3 计划完成后 复杂多文件实现后 编写测试但未运行前 对当前进展感到不确定时
.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.
执行ship-safe安全与质量审计,检查密钥、漏洞及风险模式。需在启用环境变量后使用,适用于PR前或合并前。根据严重性分类结果:高危阻断,中低危提示,确保代码发布安全。
在提交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 格式的验收标准文件。遵循状态机流程,确保故事经人类审批后进入发现阶段。包含文件模板、验证脚本及复杂度场景指南,旨在以最小规格驱动开发。
需要为新功能编写验收标准时 创建或更新 docs/stories/ 下的故事文件时
.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
评估新功能上线前的运维就绪状态,涵盖故障模式分析、可观测性指标(Metrics/Logs/Traces)配置及告警规则。提供标准化Runbook模板以指导症状诊断、缓解措施及升级流程,确保生产环境稳定性与快速恢复能力。
新功能上线前评审 生产发布准备检查 服务变更风险评估
.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的双向一致性。文件主导叙事和Gherkin,Issue主导标签、状态及DoD。支持新建同步(File→Issue)及正文更新同步,确保两端数据实时对齐。
需要创建新的GitHub Issue以关联故事文件时 故事文件的Gherkin或验收标准发生更改需同步至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支付集成最佳实践,涵盖Webhook签名验证、幂等性处理及订阅管理。包含本地测试命令、TypeScript代码示例及安全规范,指导开发者安全高效地实现支付功能。
集成Stripe支付接口 配置Webhook接收器 处理订阅创建与续费 解决支付幂等性问题
.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、追加式迁移和 TypeScript 类型生成,确保数据安全和开发效率。
构建 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开发流程。QA编写失败测试,专家实现最小代码使测试通过并重构。支持ATDD模式及集成测试容器生命周期管理,确保先写测试后实现,严禁削弱断言或使用不必要的Mock。
需要实现新功能时 执行红绿重构循环时 进行ATDD验收测试驱动开发时
.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基础设施即代码的最佳实践,包括模块目录结构、资源定义模式及工作流。强调安全规范:执行前必须验证和规划,严禁未授权Apply,需标注成本估算并启用状态锁定与标签管理。
编写或重构Terraform配置 设计IaC模块结构 询问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框架对组件和信任边界进行威胁建模,生成包含资产、风险分析及缓解措施的威胁注册表。适用于功能发布前的安全审查,确保关键风险得到控制。
新功能安全审查 系统架构变更评估 敏感数据处理流程设计
.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/Node 运行时选择、环境变量管理及中间件模式的最佳实践。指导如何配置 vercel.json、安全头设置及本地构建流程,确保高效安全的 Next.js 应用部署。
需要部署 Next.js 应用到 Vercel 配置 Vercel 环境变量或中间件 选择 Edge 或 Node.js 运行时 处理 Vercel 路由重写或重定向
.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) 处理TUI终端样式适配
.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工具。针对web和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
根据设计令牌和原型自动生成完整的UI组件文件树,包含实现代码、Storybook故事、单元测试及Gherkin规范。支持React-Mantine/Tailwind等框架,生成含TODO占位符的可运行骨架代码。
需要新建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文件,并自动生成对应测试框架的步骤定义存根。支持TypeScript、Python和Java栈,保持文档与测试同步。
需要同步用户故事到测试套件 从Markdown中提取Gherkin语法 生成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 主题对象。该技能确保设计令牌作为唯一事实来源,所有框架输出均由此派生再生。
修改或生成设计令牌 更新品牌颜色、排版、间距等设计系统值 需要同步前端样式与后端/设计令牌
.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文件 创建.dockerignore 进行容器镜像安全扫描
.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注释格式、架构审查问题清单及月度成本估算模板,辅助基础设施变更审查与新服务设计。
审查基础设施变更 设计新服务时评估成本
.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全生命周期,支持创建、查看、编辑标签与指派、评论及关闭。提供TDD状态等标准化评论模板,并通过引用和标签实现Issue间的父子或阻塞关联,无需人工干预即可自动化故事制品管理。
需要创建新的GitHub Issue作为故事制品时 需要更新Issue的状态、标签、指派或添加评论时 需要建立Issue之间的依赖或关联关系时 查询特定阶段或被阻塞的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中幂等地同步Labels和Milestones。通过upsert逻辑确保标签存在且属性正确,涵盖阶段、类型、优先级及设计等分类,仅新增或更新,不删除。
新项目初始化配置 CI流程中同步标签状态 需要统一团队标签规范时
.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卡片、查询字段选项ID及更新单选字段值。优先使用gh CLI,无则回退至GraphQL API。
需要向GitHub项目看板添加Issue 需要更新项目看板的字段状态或类型
.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故事文件 更新现有的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作为任务的管理(创建/读取/更新/评论/关闭)及Pull Request操作。适用于所有与GitHub交互的场景,支持自动化工作流和状态跟踪。
需要管理GitHub Issue或PR时 执行GitHub仓库相关操作时
.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生成模式,使文档、注释、提交信息和文章更自然。支持手动调用或集成MAPLE流程,通过去除29种AI写作特征及多轮审核优化文风。
使用/humanizer命令处理粘贴文本 在聊天中请求人性化文本 MAPLE Phase 7后自动调用(若配置humanize: true)
.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文件时 需要优化Jupyter Notebook结构或实现参数化执行 进行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至6的推进或手动触发。输出合规评分报告,若未达阈值则阻止流程,确保代码简洁、精准且目标明确。
Phase 5 IMPLEMENT完成后自动触发以进入Phase 6 用户通过/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模板,以及基于环境的目录结构规范和应用前验证工作流,用于编写或审查K8s清单文件。
编写或审查 Kubernetes 清单文件 使用 Kustomize 管理环境覆盖层 配置 Deployment 的资源限制与健康检查 设置 PodDisruptionBudget 或 HPA 执行 kubectl apply 前的干跑验证
.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 -
用于生成和验证五种Mermaid架构图(组件、时序、ER、部署、状态机)的规范。要求每图节点不超过30个,确保语法正确并经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技术栈生成高保真组件Mockup代码。支持Web与终端界面,需通过预检确保线框图状态及令牌文件存在。
需要基于已批准线框图生成高保真UI组件代码 实现符合项目设计令牌和特定技术栈(如Mantine、Tailwind)的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注册表安装。运行时强制遵守特定Harness指令,并维护管道状态以供UI展示进度。
用户通过/pipeline-runner命令请求运行特定名称的工作流、技能或智能体 需要跨多个阶段协调任务执行并跟踪实时进度
.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使用经济性。
编写或运行浏览器自动化测试 需要交互式探索网页元素 将手动操作步骤转换为Playwright测试脚本
.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 Schema、迁移及查询优化模式。涵盖命名规范、带RLS的表结构、索引策略、连接池配置及开发规则,确保SQL幂等性与性能。
编写PostgreSQL数据库表结构 创建或修改数据库迁移文件 优化慢查询或设计索引 配置PgBouncer连接池
.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集成最佳实践,涵盖键命名规范、Cache-Aside缓存模式、基于有序集合的限流及Session存储实现。包含TTL设置、SCAN替代KEYS、管道优化等关键规则,助力构建高性能可靠的Redis服务。
需要设计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.
提供计划、代码和测试的独立审查意见。在规划完成、复杂实现或编写测试后调用,生成CRITICAL/WARN/INFO列表及批准/修改建议,辅助主代理发现遗漏并决策。
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前或合并前运行,根据严重程度(高/中/低)报告结果并决定是否可以发布。
准备提交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 验收标准故事文件。通过状态机管理草稿、审批流程,并内置脚本验证故事文件的完整性与批准状态,确保需求明确且可执行。
需要为功能编写验收标准时 检查或验证现有故事文件是否已获批
.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并回填编号,以及同步正文更新。
需要创建新的GitHub Issue以关联故事文件时 故事文件的Gherkin或验收标准发生修改需同步至Issue时
.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 迁移模板及 Edge Function 安全模式。强调始终启用 RLS、禁止客户端暴露 service_role 密钥,以及使用类型生成工具确保类型安全。
构建 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模式及Docker集成测试生命周期管理,确保测试先行、无回归且行为不变。
实现新功能 编写单元测试 执行重构 运行集成测试
.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基础设施即代码的最佳实践,包括模块结构、状态管理及变量规范。涵盖目录组织、资源编写模式及CI/CD工作流。强调验证、计划及安全规则,强制成本标注与环境隔离,确保IAC的规范性与可维护性。
编写或重构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部署最佳实践,涵盖vercel.json配置、Edge/Node运行时选择、环境变量管理及CI/CD工作流。包含中间件安全模式与构建规范,适用于Next.js在Vercel上的部署优化。
用户询问如何在Vercel上部署Next.js应用 需要配置Vercel的vercel.json文件 讨论Vercel Edge函数或中间件实现 涉及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和TUI终端目标,确保色彩符合WCAG对比度标准,需人工审核后方可生效。
建立品牌视觉识别 需要生成设计令牌(Design Tokens) 初始化项目视觉风格
.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)。支持Web和TUI目标,输出包含ASCII布局、Excalidraw可编辑图及HTML预览。需人工审批后方可进入后续设计阶段。
创建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无障碍审计,支持axe-core或pa11y工具。当故事含ui:true或需审计时触发。自动检测工具并运行检查,输出违规报告至PR评论,阻断AA级违规合并,同时兼容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或HTML技术栈,生成含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文件及步骤定义存根,保持测试与文档同步。支持TypeScript、JavaScript、Python和Java框架,自动检测项目栈并处理代码块提取。
需要将从故事文档到自动化测试的Gherkin场景同步 为新功能或修改生成Cucumber步骤定义存根
.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格式的设计令牌(tokens.json),并自动生成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最佳实践,涵盖多阶段构建、健康检查及安全规则。用于编写或审查Dockerfile与Compose配置,确保生产环境使用非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
用于审查架构决策的云成本影响。涵盖计算、存储、网络及第三方API的费用驱动因素,提供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全生命周期,支持创建、查看、编辑标签与指派、评论及关闭。通过标准化输入输出和特定角色评论模板,实现故事从PO创建到QA关闭的自动化流转与关联。
需要创建或更新GitHub Issue以追踪故事进度时 在故事各阶段(如开发、测试)自动记录状态变更并添加评论时 需要链接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/CD 及项目启动时标签状态一致,支持阶段、类型、优先级等多维分类管理。
新项目初始化设置 CI/CD 流水线中保证标签一致性 同步或更新 GitHub 仓库的标签定义
.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。
需要向 GitHub 项目看板添加 Issue 需要更新项目看板的字段值或状态 需要查询或管理 GitHub Projects v2 看板内容
.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 分配、时间戳及命名。提供文件模板、标签格式、验收标准及 DoD。包含严格的验证规则,如单一职责、GWT 顺序及声明式步骤,确保故事文件的一致性与质量。
编写新的用户故事文件 更新现有故事文件内容 需要分配故事 ID 或生成文件名时
.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 Requests 操作。支持通过标签、状态和搜索过滤进行高效协作与自动化工作流集成。
需要创建或管理 GitHub Issue 需要处理 Pull Request 查询仓库状态或分支信息 执行 GitHub Actions 相关操作
.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写作特征,适用于合并前的最终润色。
@docs生成文档后 提交代码前检查提交信息 需要让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结构或实现可复现性时 使用Papermill或nbconvert工具时
.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资源清单 审查K8s配置文件 使用Kustomize管理环境覆盖
.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技术栈生成高保真组件Mockup代码。支持Web与TUI目标,含预检逻辑及模板。
需要基于线框图生成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注册表自动安装缺失技能。运行时强制遵循各IDE指令文件及Gherkin范围约束,并实时更新状态以支持可视化监控。
用户请求运行特定名称的工作流、技能或智能体 需要统一入口调度多个步骤的复杂任务
.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测试,旨在高效完成浏览器自动化与测试开发。
编写或运行浏览器自动化测试 需要交互式探索网页元素 进行基于快照的页面交互操作
.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模式、迁移及查询优化规范。涵盖表结构(含RLS)、索引策略、连接池配置及幂等SQL编写原则,确保数据库变更安全高效。
编写PostgreSQL数据库表结构 创建或更新数据库迁移脚本 优化复杂SQL查询性能 配置PgBouncer连接池
.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替代KEYS及管道操作,确保高性能与稳定性。
需要实现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.
用于规范化和记录重大架构决策的 RFC/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.
模拟橡皮鸭调试,在计划、实现或测试完成后提供独立审查。支持计划、代码和测试三种模式,输出CRITICAL/WARN/INFO列表及批准/请求更改裁决,帮助捕获遗漏问题。
完成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 前或合并前使用。按严重性分类输出:高危及致命为阻塞项需修复,中低危为建议,全部通过则放行。
准备提交 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 目录 验证现有故事文件的合规性或批准状态
.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
评估新功能上线前的运维就绪状态,涵盖故障模式分析、可观测性指标(Metrics/Logs/Traces)及告警配置,并提供标准化的应急操作手册模板。
新功能发布前审查 生产环境上线准备检查
.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的双向一致性。文件主导叙事,Issue主导状态和标签。支持从文件创建Issue、更新Issue内容,并将Issue元数据回写至文件Frontmatter,确保两端信息同步。
需要同步故事文件状态到GitHub Issue时 需要基于故事文件创建新的GitHub 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、禁止客户端暴露 service_role 密钥、保持迁移追加式更新及使用类型生成确保类型安全。
构建 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开发流程,包含红绿重构循环及ATDD模式。明确QA与专家角色分工,规范测试先行、最小实现及重构步骤。新增集成测试容器生命周期管理,确保端到端验证的自动化与环境隔离。
实现新功能 编写单元测试 执行重构 运行集成测试
.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 基础设施即代码的最佳实践,涵盖模块目录结构、资源定义规范及工作流。强调安全合规,如状态锁定、环境隔离和 FinOps 成本标注,并严格规定执行计划与验证流程,禁止未经指令的自动应用。
编写或重构 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框架对组件和信任边界进行威胁建模,生成包含资产、风险分析及缓解措施的威胁注册表。适用于功能发布前的安全审查,输出至指定文档路径。
新功能安全审查 系统架构变更评估 生成威胁模型报告
.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 设置、运行时选择(Edge/Node.js)、ISR 缓存、CLI 命令操作、中间件模式及安全头部规则,用于优化 Next.js 在 Vercel 上的开发与部署流程。
部署到 Vercel 配置 Vercel 环境 Next.js 中间件开发
.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对比度标准,需人工审核后方可应用。
需要建立或更新品牌视觉规范 从文字描述生成设计令牌 为Web或TUI界面创建配色方案
.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

Home - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-14 20:41
浙ICP备14020137号-1 $Map of visitor$