Agent Skills › kinncj/Heimdall

kinncj/Heimdall

GitHub

对生成UI执行WCAG 2.2 AA级无障碍审计,支持axe或pa11y工具。针对web目标使用浏览器工具并阻断合并违规;针对tui目标则基于终端清单评估并生成标准化报告。适用于所有含ui:true的故事。

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级无障碍审计,支持axe或pa11y工具。针对web目标使用浏览器工具并阻断合并违规;针对tui目标则基于终端清单评估并生成标准化报告。适用于所有含ui:true的故事。
故事包含 ui:true 属性 用户明确要求进行无障碍审计
.claude/skills/a11y-audit/SKILL.md
npx skills add kinncj/Heimdall --skill a11y-audit -g -y
SKILL.md
Frontmatter
{
    "name": "a11y-audit",
    "description": "Run WCAG 2.2 Level AA accessibility audits against generated UI. Use when a story has ui:true or when asked to audit accessibility."
}

SKILL: a11y-audit

Purpose

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

Target awareness

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

WCAG 2.2 AA — Minimum Requirements

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

Tool Detection

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

Run with axe-core CLI

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

mkdir -p docs/design/mockups

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

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

Run with pa11y

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

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

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

Parse Results and Classify

import json, sys

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

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

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

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

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

Post Findings as PR Comment

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

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

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

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

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

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

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

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

Merge Gate Check

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

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

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

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

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

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

Manual Audit Checklist (no tool available)

When TOOL=none, perform a structured manual check:

## Manual A11y Checklist — {story_id}

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

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

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

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

Failure Modes

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

Logging

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

SKILL: component-scaffold

Purpose

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

Inputs

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

Output Tree

For PasswordResetForm with stack react-mantine:

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

Generate Component Index (react-mantine)

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

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

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

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

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

export default {ComponentName};

Generate Storybook Story

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

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

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

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

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

Generate Unit Test

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

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

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

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

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

Generate Gherkin Step Binding

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

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

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

Python Stack Alternative

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

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

Scaffold Script

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

mkdir -p "$DIR"

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

Token Wiring

After scaffold, inject token import into index.tsx:

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

Failure Modes

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

Logging

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

SKILL: cucumber-automation

Purpose

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

Supported Stacks

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

Detect the stack from the repo root:

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

Extract Gherkin from a Story File

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

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

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

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

Write the Feature File

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

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

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

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

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

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

Generate Step Definition Stubs (TypeScript)

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

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

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

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

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

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

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

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

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

Generate Step Definition Stubs (Python / behave)

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

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

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

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

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

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

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

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

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

Batch Sync (all stories → all feature files)

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

Playwright + behave Integration

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

environment.py template

import json
import threading
from playwright.sync_api import sync_playwright

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

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

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

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

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

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

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

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

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

Browser capability scenarios

# Geolocation granted — default, handled by before_scenario

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

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

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

Loading state (route with threading.Event)

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

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

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

API error state

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

Antipatterns checklist (rubber duck will flag these)

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

Failure Modes

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

Logging

[cucumber] WRITE   tests/features/user-auth/reset-password.feature  scenarios=3
[cucumber] STUBS   tests/step-definitions/user-auth/reset-password.steps.ts  steps=7
[cucumber] SKIP    tests/step-definitions/user-auth/login.steps.ts  (already exists)
[cucumber] NO_GHERKIN  docs/stories/auth-spike-20250416143000-0002.md  (spike — expected)
用于读取和写入 W3C DTCG 格式的设计令牌,并生成 CSS、Tailwind 和 Mantine 输出。以 tokens.json 为唯一数据源,自动派生框架特定配置,禁止手动编辑生成文件。
修改或生成设计令牌 更新品牌色、排版或间距等设计系统变量 同步设计令牌到前端框架配置
.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和Compose最佳实践,包括多阶段构建、健康检查及安全规则。用于编写或审查Dockerfile和配置文件,确保使用非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 全生命周期,包括创建、查看、更新标签与指派、添加评论及关闭。支持通过命令行工具 gh 操作,关联故事文件,并处理 TDD 状态和阻塞通知等场景。
需要创建或更新 GitHub Issue 管理故事(Story)的 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流程中强制同步标签定义 需要统一团队工作流分类(如阶段、类型、优先级)时
.claude/skills/gh-labels-milestones/SKILL.md
npx skills add kinncj/Heimdall --skill gh-labels-milestones -g -y
SKILL.md
Frontmatter
{
    "name": "gh-labels-milestones",
    "description": "Bootstrap and sync GitHub labels and milestones idempotently. Use when setting up a new repository or syncing label definitions."
}

SKILL: gh-labels-milestones

Purpose

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

Label Bootstrap

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

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

Label Groups

Phase Labels (pipeline position)

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

Type Labels (work classification)

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

Priority Labels (MoSCoW)

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

Spec / Story Kit Labels

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

Design Labels

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

ADR Labels

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

UI / Accessibility Labels

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

Status Labels

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

Milestone Bootstrap

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

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

Assign Labels to an Issue

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

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

Failure Modes

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

Logging

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

SKILL: gh-projects

Purpose

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

Inputs

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

Read project config

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

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

Add an Issue to the Project Board

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

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

Save the returned item_id — required for field updates.

Get Field and Option IDs

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

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

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

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

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

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

Update a Text Field (Epic, Specialist)

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

Standard Field Updates by Pipeline Phase

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

Query Board State

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

Failure Modes

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

Logging

[gh-projects] ADD    #42  → project #{project_number}  item_id={id}
[gh-projects] UPDATE #42  field=Status  value="In progress"
[gh-projects] SKIP   #42  already on board
用于生成符合规范命名的Gherkin故事文件,处理ID分配、时间戳及标签。提供标准模板与验证规则,确保场景单一职责、步骤顺序正确且无实现细节,适用于产品负责人编写或更新故事文档。
编写新的Gherkin用户故事 更新现有故事文件 需要生成标准化的验收测试用例
.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 全生命周期管理(创建/查询/更新/评论/关闭)及 Pull Request 操作(草稿/审查/合并)。用于自动化 GitHub 交互任务。
需要创建或管理 GitHub Issue 需要提交或处理 Pull Request 需要查看仓库状态或认证信息 涉及 GitHub Actions 或项目板操作
.claude/skills/github-cli/SKILL.md
npx skills add kinncj/Heimdall --skill github-cli -g -y
SKILL.md
Frontmatter
{
    "name": "github-cli",
    "description": "Comprehensive gh CLI reference — issues, tasks, PRs, repos, branches, Actions, search, and project board. Use whenever interacting with GitHub."
}

SKILL: GitHub CLI

Repo Context (always run first)

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

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

Issues as Tasks

Create

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

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

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

Read

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

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

Update

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

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

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

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

Comment

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

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

Close

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

Pull Requests

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

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

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

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

# Check CI on PR
gh pr checks {number}

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

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

Branches

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

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

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

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

Actions / CI

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

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

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

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

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

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

Repo Inspection

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

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

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

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

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

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

Labels & Milestones

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

# List labels
gh label list

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

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

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

Search

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

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

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

Issue Lifecycle (MAPLE Standard)

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

Failure Modes

Condition Action
Not authenticated gh auth login — do not retry ops
Label missing Create via gh label create before using
Milestone missing Create via gh api milestones before using
Rate limited Wait 60s, retry once; if fails again, stop
Issue already exists gh issue list --search "{title}" --state all — return existing number
gh not found Stop. Output: "gh CLI not available. Install from https://cli.github.com"
移除文本中可检测的AI生成模式,使文档、注释和文章更自然。适用于`@docs`后、提交前及品牌一致性校准,支持自定义风格匹配,通过三轮处理消除29种AI写作特征。
@docs生成文档后 提交代码前检查 人工语音校准 让AI辅助文本更自然
.claude/skills/humanizer/SKILL.md
npx skills add kinncj/Heimdall --skill humanizer -g -y
SKILL.md
Frontmatter
{
    "name": "humanizer",
    "description": "Remove AI-isms and artificial language patterns from text. Makes documentation, comments, commit messages, and prose sound more natural and human. Based on Wikipedia's \"Signs of AI writing\" patterns."
}

humanizer skill

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

When to use

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

How to invoke

/humanizer

[paste your text here]

Or ask directly in chat:

Please humanize this text: [your text]

Voice calibration (optional)

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

/humanizer

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

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

What it detects

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

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

Integration with MAPLE

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

Output

Returns a humanized version of your text with:

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

Further reading

提供Jupyter Notebook最佳实践,包括标准单元格结构、Papermill参数化执行、nbformat编程创建及nbconvert导出。强调Git卫生,如清理输出和版本控制规范,确保笔记本的可复现性与整洁性。
处理.ipynb文件时 需要优化Jupyter Notebook结构或可复现性时 使用Papermill进行参数化执行时 通过代码动态生成Notebook时 配置Git以管理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部署配置 审查K8s资源清单 使用Kustomize管理环境覆盖
.claude/skills/kubernetes-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill kubernetes-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "kubernetes-patterns",
    "description": "Apply Kubernetes and Kustomize patterns for deployments, services, and overlays. Use when writing or reviewing k8s manifests."
}

SKILL: Kubernetes Patterns

Kustomize Structure

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

Deployment with Required Fields

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

PodDisruptionBudget

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

HPA

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

Validation Workflow

# Always validate before applying
kubectl apply --dry-run=client -f {manifest}
kustomize build overlays/dev | kubectl apply --dry-run=client -f -
helm template {release} {chart} | kubectl apply --dry-run=client -f -
用于生成和验证功能架构的 Mermaid 图表,涵盖组件、时序、ER、部署及状态机图。遵循节点限制与语法规范,确保文档准确性。
需要绘制系统架构图时 记录功能模块交互逻辑时 设计数据库实体关系时 描述应用部署拓扑时 定义业务状态流转时
.claude/skills/mermaid-diagrams/SKILL.md
npx skills add kinncj/Heimdall --skill mermaid-diagrams -g -y
SKILL.md
Frontmatter
{
    "name": "mermaid-diagrams",
    "description": "Generate and validate Mermaid diagrams (component, sequence, ER, deployment, state machine) for features. Use when documenting architecture."
}

SKILL: Mermaid Diagrams

Required Diagrams Per Feature

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

Rules

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

Component Diagram

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

Sequence Diagram

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

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

ER Diagram

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

State Machine

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

Deployment Diagram

graph TB
    subgraph "Vercel Edge"
        FE["Next.js App"]
        EF["Edge Functions"]
    end
    subgraph "AWS"
        LB["Load Balancer"]
        APP["App Servers"]
        RDS[("RDS PostgreSQL")]
        EC["ElastiCache Redis"]
    end
    FE --> EF
    EF --> LB
    LB --> APP
    APP --> RDS
    APP --> EC
根据已审批的线框图和设计令牌,为指定UI技术栈生成高保真组件Mockup代码。支持Web和终端界面,包含预检逻辑与模板。
需要基于线框图生成高保真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注册表自动安装缺失技能,并将运行状态实时写入maple.json以支持UI监控。
用户希望运行指定的工作流、技能或代理任务 需要跨不同组件(工作流/技能/代理)进行统一调用和状态追踪
.claude/skills/pipeline-runner/SKILL.md
npx skills add kinncj/Heimdall --skill pipeline-runner -g -y
SKILL.md
Frontmatter
{
    "name": "pipeline-runner",
    "description": "Universal dispatcher: run a named taffy workflow (.claude\/taffy\/<name>.yaml), a skill (\/skill-name), or a sub-agent (@agent-name). Falls back to skills.sh registry when a skill is not found locally. Tracks all runs in .claude\/state\/maple.json so the maple TUI shows live progress."
}

SKILL: pipeline-runner

What It Does

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

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

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

Usage

/pipeline-runner <name>

Examples:

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

List available taffy workflows:

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

List available skills:

ls .claude/skills/

Dispatch Protocol

Step 1: Resolve the target

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

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

Step 2: Initialise state

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

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

Step 2b: Runtime policy enforcement (mandatory)

Before any stage execution, read and enforce:

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

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

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

Step 3a: Taffy workflow execution

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

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

For each stage:

when: guard:

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

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

Dispatch:

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

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

Progress heartbeats (mandatory):

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

Completion artifact gate (mandatory):

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

Step 3b: Skill invocation

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

Step 3c: Agent delegation

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

Step 4: Human-approval gates (taffy only)

When a stage has gate: human-approval:

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

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

Step 5: Completion

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

Output:

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

Failure Handling

After 3 consecutive failures on any stage:

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

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

Rate-limit Handling

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

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

Session Context

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

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

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

State File Reference

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

.claude/state/maple.json

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

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

.claude/state/approval-pending.txt

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

.claude/state/rate-limit.txt

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

.claude/state/sessions.json

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

Skip Conditions

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

SKILL: Playwright CLI

When to Use

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

Installation

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

Core CLI Commands

Browser Exploration

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

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

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

Session Management

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

Snapshot-to-Test Workflow

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

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

Test Execution

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

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

# Visual debugger
npx playwright test --ui

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

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

# View HTML report
npx playwright show-report

API Testing (Playwright request fixture)

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

Token Efficiency Note

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

提供PostgreSQL数据库模式、迁移及查询的最佳实践。涵盖表结构定义、行级安全(RLS)配置、索引优化策略、PgBouncer连接池设置及严格的变更规则,确保SQL编写的规范性与高性能。
编写PostgreSQL表结构或迁移脚本 优化SQL查询性能 配置数据库连接池 实施行级安全策略
.claude/skills/postgresql-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill postgresql-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "postgresql-patterns",
    "description": "Apply PostgreSQL schema, migration, and query patterns with idempotent SQL. Use when writing database schemas or migrations."
}

SKILL: PostgreSQL Patterns

Migration Naming

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

Table Pattern with RLS

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

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

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

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

-- RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

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

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

Query Optimization

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

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

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

Connection Pooling (PgBouncer)

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

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

Rules

  • NEVER modify existing migration files.
  • Always create new migration for schema changes.
  • Index all foreign keys.
  • Use CONCURRENTLY for index creation on large tables.
  • EXPLAIN ANALYZE all queries > 100ms.
  • Enable RLS on tables with user data.
提供 Redis 集成最佳实践,涵盖键命名规范、Cache-Aside 缓存模式、基于有序集合的限流及会话存储实现。包含关键规则如设置 TTL、使用 SCAN 和管道操作,助力高效构建高可用服务。
需要实现数据缓存逻辑 设计 API 限流策略 配置用户会话存储
.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.
提供计划、代码和测试的独立第二意见审查。在规划完成、复杂实现或编写测试后触发,输出CRITICAL/WARN/INFO列表及审批结论,帮助发现遗漏问题并指导后续迭代。
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前或合并前运行,按严重性报告发现项,阻断高危问题以确保代码安全上线。
提交PR前 合并功能分支到主分支前 添加新依赖后 修改认证或基础设施配置后
.claude/skills/ship-safe/SKILL.md
npx skills add kinncj/Heimdall --skill ship-safe -g -y
SKILL.md
Frontmatter
{
    "name": "ship-safe",
    "description": "Run ship-safe security and quality audit on the current project. Executes npx ship-safe audit . and reports findings by severity. Use before shipping any feature or PR."
}

SKILL: Ship-Safe Audit

What It Does

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

Usage

npx ship-safe audit .

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

Opt-In

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

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

When to Use

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

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

Output Interpretation

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

Agent Instructions

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

Example Integration (architect / pre-ship checklist)

/ship-safe

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

在实现前生成Gherkin格式的故事文件作为最小规格说明。遵循状态机流程,包含模板、验证脚本及场景数量指南,确保故事经人工批准后方可进入发现阶段。
需要为功能编写Gherkin验收标准时 初始化新功能的故事文档时
.claude/skills/spec-kit/SKILL.md
npx skills add kinncj/Heimdall --skill spec-kit -g -y
SKILL.md
Frontmatter
{
    "name": "spec-kit",
    "description": "Write a Gherkin story file to docs\/stories\/ for a feature. The story IS the spec. No intermediate PROBLEM\/SPEC\/PLAN\/TASKS artifacts."
}

SKILL: spec-kit

Purpose

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

State Machine

feature description → story draft → human approval → DISCOVER

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

Story File Location

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

Story File Template

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

## Story

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

## Acceptance Criteria

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

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

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

Definition of Done

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

## Validate a Story File

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

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

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

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

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

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

Check All Stories Are Approved

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

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

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

exit $FAIL

Scenario Count Guidelines

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

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

Skip Conditions

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

Failure Modes

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

Logging

[spec-kit] WRITE   docs/stories/user-auth-reset-password.md
[spec-kit] HALT    awaiting human approval
[spec-kit] OK      docs/stories/user-auth-reset-password.md  status=approved
[spec-kit] SKIP    spike/* branch — spec-kit not required
用于评估新功能上线前的运维就绪状态。涵盖故障模式分析、可观测性指标/日志/链路追踪要求、告警策略及标准演练手册模板,确保服务稳定性与快速恢复能力。
新功能上线前审查 生产环境发布准备评估 SLO和告警配置审核
.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的双向一致性。以文件为叙事权威,Issue为状态/标签权威。支持新建时从文件同步至Issue并回填编号,以及编辑文件后更新Issue内容。
需要同步新创建的故事文件到GitHub Issue 需要更新已存在的Issue正文以匹配故事文件变更
.claude/skills/story-issue-sync/SKILL.md
npx skills add kinncj/Heimdall --skill story-issue-sync -g -y
SKILL.md
Frontmatter
{
    "name": "story-issue-sync",
    "description": "Maintain bidirectional consistency between story markdown files and GitHub Issues. Use when syncing story status with GitHub."
}

SKILL: story-issue-sync

Purpose

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

Ownership Model

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

Inputs / Outputs

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

Create: File → Issue

When a new story file exists with issue_number: null:

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

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

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

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

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

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

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

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

Update: File → Issue (body changed)

When story Gherkin or acceptance criteria are edited:

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

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

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

Sync: Issue → File (labels changed)

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

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

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

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

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

Detect Drift (file vs issue)

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

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

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

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

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

Batch Sync (all stories)

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

Failure Modes

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

Logging

[story-sync] CREATED       #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] BODY_UPDATE   #42  ← docs/stories/user-auth-reset-0001.md
[story-sync] LABELS_SYNC   #42  → docs/stories/user-auth-reset-0001.md
[story-sync] DRIFT         #42  title mismatch — issue updated
[story-sync] SKIP          #42  no changes detected
提供Stripe支付集成最佳实践,涵盖Webhook安全验证、幂等性处理及订阅管理。包含本地测试命令、TypeScript代码示例及安全规范,确保支付流程稳定可靠。
需要实现Stripe Webhook接收与处理 创建Stripe Checkout Session或订阅 集成Stripe支付功能时涉及幂等性需求
.claude/skills/stripe-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill stripe-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "stripe-patterns",
    "description": "Apply Stripe API integration patterns: webhooks, idempotency keys, and subscription handling. Use when integrating Stripe payments."
}

SKILL: Stripe Patterns

Local Testing Setup

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

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

# Monitor logs
stripe logs tail

Webhook Handler Pattern

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

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

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

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

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

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

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

Checkout Session Pattern

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

Rules

  • ALWAYS verify webhook signatures with constructEvent().
  • ALWAYS use idempotency keys on write operations.
  • NEVER log raw card data or full payment method details.
  • Use restricted API keys with minimum required permissions per service.
  • Test every webhook handler with stripe trigger.
  • Store Stripe customer IDs in your database for lookup.
提供 Supabase 开发最佳实践,涵盖本地工作流、SQL 迁移模板及 Edge Function 安全模式。强制启用 RLS,规范用户数据访问策略与类型生成,确保构建过程的安全性与一致性。
需要创建 Supabase 数据库表或配置 RLS 策略 编写 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模式及集成测试容器生命周期管理,确保测试先行、行为不变且无回归。
实现新功能 执行TDD流程 运行集成测试
.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基础设施即代码的模块结构、状态管理及变量最佳实践。涵盖目录规范、HCL示例、工作流命令及安全规则,指导编写可维护、成本可控且安全的Terraform配置。
编写或重构Terraform代码 设计基础设施模块结构 配置Terraform状态管理
.claude/skills/terraform-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill terraform-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "terraform-patterns",
    "description": "Apply Terraform module structure, state management, and variable patterns for infrastructure as code. Use when writing Terraform."
}

SKILL: Terraform Patterns

Module Structure

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

Module Pattern

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

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

  username = var.username
  password = var.password

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

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

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

Workflow

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

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

# Validate syntax
terraform validate

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

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

# State inspection
terraform show
terraform state list

Rules

  • ALWAYS run terraform validate before reporting complete.
  • ALWAYS run terraform plan to show what will change.
  • NEVER run terraform apply without explicit instruction.
  • ALWAYS annotate resources with # finops: cost estimate.
  • Use workspaces or separate state files per environment.
  • Enable state locking (S3 + DynamoDB or Terraform Cloud).
  • Tag all resources with environment, managed-by, and cost-center.
基于STRIDE框架对组件和信任边界进行威胁建模,生成包含资产、风险分析、缓解措施及发布前检查清单的威胁登记册,用于功能安全审查。
执行新功能的安全评审 识别系统架构中的潜在威胁 制定安全缓解措施
.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 部署最佳实践,涵盖 vercel.json 配置、Edge/Node.js 运行时选择、环境变量管理及 CI/CD 工作流。包含安全头设置、中间件模式及 ISR 配置指南,指导高效安全的 Next.js 应用部署。
Vercel 部署配置 Next.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对比度检查,需人工审批后生效。
建立品牌视觉风格 生成设计令牌文件 需要符合WCAG标准的色彩方案
.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目标则基于终端清单评估并输出标准化JSON结果。
故事包含 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
根据设计令牌和原型自动生成带测试及Gherkin规范的完整UI组件骨架,支持自动检测技术栈。
需要生成新的UI组件文件结构 基于设计令牌创建组件骨架
.cursor/skills/component-scaffold/SKILL.md
npx skills add kinncj/Heimdall --skill component-scaffold -g -y
SKILL.md
Frontmatter
{
    "name": "component-scaffold",
    "description": "Generate a complete component file tree wired to design tokens with tests and Gherkin spec. Use when scaffolding a new UI component."
}

SKILL: component-scaffold

Purpose

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

Inputs

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

Output Tree

For PasswordResetForm with stack react-mantine:

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

Generate Component Index (react-mantine)

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

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

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

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

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

export default {ComponentName};

Generate Storybook Story

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

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

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

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

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

Generate Unit Test

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

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

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

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

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

Generate Gherkin Step Binding

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

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

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

Python Stack Alternative

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

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

Scaffold Script

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

mkdir -p "$DIR"

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

Token Wiring

After scaffold, inject token import into index.tsx:

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

Failure Modes

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

Logging

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

SKILL: cucumber-automation

Purpose

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

Supported Stacks

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

Detect the stack from the repo root:

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

Extract Gherkin from a Story File

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

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

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

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

Write the Feature File

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

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

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

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

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

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

Generate Step Definition Stubs (TypeScript)

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

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

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

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

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

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

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

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

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

Generate Step Definition Stubs (Python / behave)

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

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

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

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

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

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

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

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

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

Batch Sync (all stories → all feature files)

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

Playwright + behave Integration

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

environment.py template

import json
import threading
from playwright.sync_api import sync_playwright

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

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

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

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

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

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

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

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

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

Browser capability scenarios

# Geolocation granted — default, handled by before_scenario

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

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

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

Loading state (route with threading.Event)

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

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

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

API error state

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

Antipatterns checklist (rubber duck will flag these)

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

Failure Modes

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

Logging

[cucumber] WRITE   tests/features/user-auth/reset-password.feature  scenarios=3
[cucumber] STUBS   tests/step-definitions/user-auth/reset-password.steps.ts  steps=7
[cucumber] SKIP    tests/step-definitions/user-auth/login.steps.ts  (already exists)
[cucumber] NO_GHERKIN  docs/stories/auth-spike-20250416143000-0002.md  (spike — expected)
用于读取和写入W3C DTCG格式的设计令牌(tokens.json),并自动生成CSS自定义属性、Tailwind配置及Mantine主题对象。tokens.json为唯一数据源,其他输出均为派生文件。
修改或生成设计令牌 更新品牌色、排版或间距等UI规范 同步前端框架(如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文件 容器化Node.js服务
.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全生命周期,支持创建、查看、编辑标签与指派人、添加评论及关闭。提供按角色分类的评论模板,支持通过引用链接Issue关系,并列出特定阶段或阻塞问题,实现故事制品的自动化流转。
需要创建新的GitHub Issue以记录故事需求时 在故事不同阶段(如发现、架构、实施)更新Issue状态或标签时 Agent执行任务受阻需请求人工介入时 验证测试通过或失败需记录状态时
.cursor/skills/gh-issues/SKILL.md
npx skills add kinncj/Heimdall --skill gh-issues -g -y
SKILL.md
Frontmatter
{
    "name": "gh-issues",
    "description": "Create, read, update, and link GitHub Issues as story artifacts throughout the story lifecycle. Use when managing issues for stories."
}

SKILL: gh-issues

Purpose

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

Inputs

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

Outputs

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

Create an Issue

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

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

View an Issue

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

Edit Labels and Assignee

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

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

Add a Comment

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

Comment conventions by role:

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

Close an Issue

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

List Issues

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

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

Link Issues (parent / blocks)

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

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

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

Failure Modes

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

Logging

Always log after every gh issue mutation:

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

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

用于幂等初始化GitHub仓库的标签和里程碑。通过脚本自动创建或更新缺失/变更的配置,确保项目阶段、类型、优先级及规范状态的一致性,适用于新项目启动和CI流程。
初始化新GitHub仓库 CI流水线中同步标签配置 需要统一团队标签命名规范时
.cursor/skills/gh-labels-milestones/SKILL.md
npx skills add kinncj/Heimdall --skill gh-labels-milestones -g -y
SKILL.md
Frontmatter
{
    "name": "gh-labels-milestones",
    "description": "Bootstrap and sync GitHub labels and milestones idempotently. Use when setting up a new repository or syncing label definitions."
}

SKILL: gh-labels-milestones

Purpose

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

Label Bootstrap

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

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

Label Groups

Phase Labels (pipeline position)

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

Type Labels (work classification)

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

Priority Labels (MoSCoW)

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

Spec / Story Kit Labels

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

Design Labels

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

ADR Labels

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

UI / Accessibility Labels

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

Status Labels

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

Milestone Bootstrap

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

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

Assign Labels to an Issue

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

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

Failure Modes

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

Logging

[gh-labels] UPSERT  label="type:feature"    color=0052cc
[gh-labels] SKIP    label="priority:high"   (unchanged)
[gh-milestones] CREATE  "v1.0"  due=2025-12-31
[gh-milestones] SKIP    "v1.0"  already exists
管理GitHub Projects v2看板,支持添加Issue卡片、查询状态及更新字段值(如状态、类型)。优先使用gh CLI,缺失时回退至GraphQL API,需读取项目配置并处理节点ID。
需要向GitHub项目看板添加Issue 需要更新项目看板的字段状态
.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分配、时间戳、文件名及标签。提供标准模板与验证规则,确保单职责、正确的GWT顺序及声明式步骤,适用于产品负责人编写或更新用户故事。
编写新的Gherkin用户故事文件 更新现有的Gherkin故事内容 需要为Epic分配下一个故事ID时
.cursor/skills/gherkin-authoring/SKILL.md
npx skills add kinncj/Heimdall --skill gherkin-authoring -g -y
SKILL.md
Frontmatter
{
    "name": "gherkin-authoring",
    "description": "Produce valid, consistently named Gherkin story files with correct IDs, timestamps, and tags. Use when writing or updating story files."
}

SKILL: gherkin-authoring

Purpose

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

Story File Naming Convention

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

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

Allocate the Next Story ID

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

Generate Filename

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

Story File Template

Every story file must contain:

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

## Story

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

## Acceptance Criteria

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

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

Definition of Done

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

ADR Links


## Gherkin Validation Rules

Before writing a scenario, verify:

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

## Background Block (shared preconditions)

```gherkin
Feature: Password Reset

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

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

Use Background when 2+ scenarios share the same preconditions.

Scenario Outline (data-driven)

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

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

Tags Reference

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

Failure Modes

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

Logging

[gherkin] ALLOCATE  epic=user-auth  next_id=0003
[gherkin] CREATE    docs/stories/user-auth-reset-password-20250416143000-0003.md
[gherkin] VALIDATE  scenarios=3  tags=ok  structure=ok
提供GitHub CLI (gh) 完整参考,涵盖仓库上下文初始化、Issue全生命周期管理(创建/查询/更新/评论/关闭)及PR操作(草稿/审查/合并)。适用于自动化处理GitHub任务流。
需要创建或管理GitHub Issue 需要提交或审查Pull Request 需要查询仓库状态或CI结果
.cursor/skills/github-cli/SKILL.md
npx skills add kinncj/Heimdall --skill github-cli -g -y
SKILL.md
Frontmatter
{
    "name": "github-cli",
    "description": "Comprehensive gh CLI reference — issues, tasks, PRs, repos, branches, Actions, search, and project board. Use whenever interacting with GitHub."
}

SKILL: GitHub CLI

Repo Context (always run first)

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

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

Issues as Tasks

Create

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

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

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

Read

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

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

Update

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

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

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

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

Comment

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

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

Close

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

Pull Requests

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

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

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

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

# Check CI on PR
gh pr checks {number}

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

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

Branches

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

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

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

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

Actions / CI

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

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

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

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

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

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

Repo Inspection

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

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

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

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

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

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

Labels & Milestones

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

# List labels
gh label list

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

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

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

Search

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

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

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

Issue Lifecycle (MAPLE Standard)

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

Failure Modes

Condition Action
Not authenticated gh auth login — do not retry ops
Label missing Create via gh label create before using
Milestone missing Create via gh api milestones before using
Rate limited Wait 60s, retry once; if fails again, stop
Issue already exists gh issue list --search "{title}" --state all — return existing number
gh not found Stop. Output: "gh CLI not available. Install from https://cli.github.com"
移除文本中的AI生成痕迹和人工模式,使文档、注释、提交信息更自然。支持通过命令或对话调用,提供语音校准以匹配个人风格,并检测29种AI写作特征进行优化。
需要去除AI写作痕迹时 在合并代码前检查提交信息和注释 @docs生成文档后 希望文本听起来更像人类自然表达时
.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编程创建笔记本、利用nbconvert导出格式及Git提交前的输出清理规范,确保代码可复现性与版本整洁。
处理.ipynb文件时 需要标准化Jupyter Notebook结构或提高可复现性时 使用Papermill进行参数化运行 程序化生成Notebook 准备提交Git时的清理操作
.cursor/skills/jupyter-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill jupyter-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "jupyter-patterns",
    "description": "Apply best practices for Jupyter notebooks: cell ordering, reproducibility, parameterisation. Use when working with .ipynb files."
}

SKILL: Jupyter Notebook Patterns

Notebook Structure

Cells should follow this order:

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

Parameterized Execution with Papermill

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

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

Programmatic Notebook Creation

import nbformat as nbf

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

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

Export

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

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

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

Git Hygiene

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

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

Rules

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

karpathy-audit skill

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

When to use

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

The 4 Principles

1. Think Before Coding

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

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

2. Simplicity First

Minimum code that solves the problem. Nothing speculative.

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

3. Surgical Changes

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

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

4. Goal-Driven Execution

Define success criteria. Loop until verified.

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

How to invoke

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

Manual call:

/karpathy-audit

or

@karpathy-audit

Audit output

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

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

Scoring & Gate Decisions

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

Scope creep detection

Compares two sources:

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

Violations flagged:

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

Phase 5 → Phase 6 Gate

Orchestrator behavior:

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

Dashboard integration

The TUI displays:

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

Remediation workflow

If audit fails or scores <70:

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

Example invocation

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

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

Further reading

提供Kubernetes和Kustomize的最佳实践模板,涵盖基础结构、Deployment配置(含安全与探针)、PDB及HPA。指导编写或审查k8s清单,并包含应用前的验证工作流。
编写或审查Kubernetes YAML文件 使用Kustomize管理多环境配置 创建Deployment、Service或HPA资源 需要验证Kubernetes清单有效性
.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、部署、状态机)以记录系统架构。遵循节点数限制及语法规范,确保图表有效并支持前端、后端及云基础设施的可视化描述。
需要绘制系统架构图时 设计数据库模型或实体关系时 梳理业务流程或状态流转时 文档化软件部署拓扑结构时
.cursor/skills/mermaid-diagrams/SKILL.md
npx skills add kinncj/Heimdall --skill mermaid-diagrams -g -y
SKILL.md
Frontmatter
{
    "name": "mermaid-diagrams",
    "description": "Generate and validate Mermaid diagrams (component, sequence, ER, deployment, state machine) for features. Use when documenting architecture."
}

SKILL: Mermaid Diagrams

Required Diagrams Per Feature

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

Rules

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

Component Diagram

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

Sequence Diagram

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

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

ER Diagram

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

State Machine

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

Deployment Diagram

graph TB
    subgraph "Vercel Edge"
        FE["Next.js App"]
        EF["Edge Functions"]
    end
    subgraph "AWS"
        LB["Load Balancer"]
        APP["App Servers"]
        RDS[("RDS PostgreSQL")]
        EC["ElastiCache Redis"]
    end
    FE --> EF
    EF --> LB
    LB --> APP
    APP --> RDS
    APP --> EC
根据已批准线框图和设计令牌,为指定UI技术栈(Mantine、Tailwind/shadcn或HTML)生成高保真组件Mockup代码。支持Web与终端界面,需通过预检确保线框图状态及令牌文件存在,输出可运行代码供人工审批。
需要基于线框图生成UI组件原型 实现已批准的Wireframe设计稿 将设计令牌转换为具体框架代码
.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指令与Gherkin范围,确保合规并实时记录状态至maple.json以支持可视化监控。
用户请求运行特定名称的工作流、技能或代理 需要统一入口调度多个步骤的任务
.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测试代码及执行测试。
需要交互式浏览网页并获取元素快照 编写或运行Playwright E2E自动化测试 进行基于快照的UI元素操作
.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数据库模式、迁移和查询的最佳实践。涵盖表结构、行级安全、索引优化及连接池配置,强调幂等性、不修改旧迁移文件及使用EXPLAIN ANALYZE进行性能分析。
编写或审查数据库表结构定义 生成或执行数据库迁移脚本 优化慢速SQL查询或设计索引策略 配置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和管道操作,确保高性能与安全性。
需要实现数据缓存策略 设计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.
用于在 docs/specs/ 下创建和管理架构决策记录(ADR)。当面临重大技术选型、新框架引入或显著模式变更时,使用此技能标准化文档结构,涵盖背景、目标、方案对比、风险及多维度影响分析。
需要记录重大的架构决策 在多个非平凡的技术方案中进行选择 引入新技术或框架 改变现有的重要设计模式
.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时 合并功能分支到主分支前 添加新依赖后 修改认证或基础设施配置后
.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的双向一致性。文件主导叙事,Issue主导状态和标签。支持创建Issue并回填编号,以及同步文件内容至Issue正文。
新建故事文件需关联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支付功能 配置Stripe Webhook接收器 处理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 开发最佳实践,涵盖本地工作流、数据库迁移及行级安全(RLS)策略模板。规范 Edge Functions 的身份验证逻辑,强调 RLS 强制启用、禁止客户端暴露 service_role 密钥及追加式迁移原则,确保数据安全性与类型安全。
需要创建 Supabase 数据库表并配置权限时 编写 Supabase Edge Function 处理用户认证时 进行 Supabase 本地开发或部署时
.cursor/skills/supabase-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill supabase-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "supabase-patterns",
    "description": "Apply Supabase schema, RLS policies, and edge function patterns. Use when building on Supabase."
}

SKILL: Supabase Patterns

Local Development Workflow

# Start local Supabase stack
supabase start

# Apply migrations
supabase db push

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

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

# Stop
supabase stop

Migration Pattern

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

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

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

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

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

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

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

Edge Function Pattern

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

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

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

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

  // ... handler logic
})

Rules

  • Enable RLS on EVERY table with user data. No exceptions.
  • Never expose service_role key to client code.
  • Always use supabase gen types typescript for type safety.
  • Migrations are append-only (never modify existing migration files).
  • Test with supabase start before deploying.
驱动基于红-绿-重构循环的TDD开发流程,确保先写测试后实现。涵盖QA编写失败测试、专家实现最小代码及重构阶段,支持ATDD模式与集成测试容器生命周期管理,严格遵循测试优先原则。
需要实现新功能 执行测试驱动开发任务
.cursor/skills/tdd-workflow/SKILL.md
npx skills add kinncj/Heimdall --skill tdd-workflow -g -y
SKILL.md
Frontmatter
{
    "name": "tdd-workflow",
    "description": "Drive development with a red-green-refactor TDD cycle, ensuring tests are written before implementation. Use when implementing any new functionality."
}

SKILL: TDD Workflow

The RED → GREEN → REFACTOR Cycle

RED Phase (QA Agent)

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

GREEN Phase (Specialist Agent)

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

REFACTOR Phase (Specialist Agent)

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

ATDD Pattern (Acceptance Test-Driven Development)

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

Integration Test Container Lifecycle

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

# Run integration tests
make test-integration

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

Rules

  • Test file is created BEFORE implementation file.
  • Test name format: "should {expected outcome} when {condition}".
  • Never weaken an assertion to make a test pass.
  • Never mock what you can test with a real dependency (use containers).
  • Implementation agent receives FILE PATH, not requirement text.
  • Gate: test must fail before implementation, pass after.
提供Terraform基础设施即代码的最佳实践,涵盖模块结构、状态管理及变量模式。包含安全合规规则,如强制验证与计划、禁止无指令应用、成本估算及资源标签,确保云资源管理的规范性与安全性。
编写或重构Terraform配置时 需要遵循IaC最佳实践和合规性检查时
.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.js 运行时选择、环境变量管理及 CI/CD 工作流。包含安全头设置和中间件模式示例,指导高效稳定的 Next.js 应用部署。
Vercel 部署配置 Next.js 运行时选择 Vercel 环境变量管理 Edge Middleware 开发
.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对比度合规,需人工审批后生效。
建立品牌视觉识别 需要生成设计令牌 配置终端界面样式
.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目标,基于Gherkin推导组件,输出需人工审批后方可进入下游设计阶段。
需要根据用户故事创建UI线框图时 收到包含story_file和ui_components的输入以生成低保真原型时
.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无障碍审计。支持Web和TUI目标,自动检测axe或pa11y工具并生成报告。拦截AA违规,强制用于ui:true的故事。
故事包含 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等框架,输出可运行的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文件,并自动生成项目测试栈(TS/JS/Python/Java)的步骤定义桩代码,保持文档与测试套件同步。
用户要求将需求文档转换为Cucumber测试用例 需要同步故事文件到测试套件 需要为新的Gherkin场景生成步骤定义存根
.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 格式的设计令牌,并生成 CSS、Tailwind 和 Mantine 输出。以 tokens.json 为唯一数据源,自动派生框架配置。
修改或生成设计令牌 更新颜色、字体等设计系统变量
.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配置,确保生产环境镜像的安全与高效。
编写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全生命周期,支持创建、查看、编辑标签与指派、评论、关闭及列表查询。通过CLI命令自动化故事从创建到QA关闭的流程,并处理父子Issue关联。
需要创建新的GitHub Issue时 更新或查看现有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流程。
新仓库初始化配置 需要统一团队标签规范 CI/CD流水线中维护Issue状态
.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 需要更新项目看板的字段状态(如 Status、Type)
.opencode/skills/gh-projects/SKILL.md
npx skills add kinncj/Heimdall --skill gh-projects -g -y
SKILL.md
Frontmatter
{
    "name": "gh-projects",
    "description": "Manage GitHub Projects v2 board: add issues, update field values, and query board state. Use when managing project boards."
}

SKILL: gh-projects

Purpose

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

Inputs

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

Read project config

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

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

Add an Issue to the Project Board

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

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

Save the returned item_id — required for field updates.

Get Field and Option IDs

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

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

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

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

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

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

Update a Text Field (Epic, Specialist)

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

Standard Field Updates by Pipeline Phase

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

Query Board State

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

Failure Modes

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

Logging

[gh-projects] ADD    #42  → project #{project_number}  item_id={id}
[gh-projects] UPDATE #42  field=Status  value="In progress"
[gh-projects] SKIP   #42  already on board
用于生成符合规范的Gherkin故事文件。处理ID分配、时间戳、文件名构造及标签格式化,确保文件命名一致并包含必要元数据。提供文件模板、Gherkin验证规则(如单一职责、声明式步骤)及验收标准,供产品负责人编写或更新用户故事时使用。
编写新的用户故事文件 更新现有的Gherkin故事文件 需要为史诗(Epic)分配下一个故事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 Request操作(创建草稿、审查、合并)及仓库上下文初始化。适用于所有GitHub交互场景。
需要创建或管理GitHub Issue 需要处理Pull Request如创建或合并 需要查询GitHub仓库状态或列表
.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辅助的文本听起来更自然时 MAPLE Phase 7后自动调用
.opencode/skills/humanizer/SKILL.md
npx skills add kinncj/Heimdall --skill humanizer -g -y
SKILL.md
Frontmatter
{
    "name": "humanizer",
    "description": "Remove AI-isms and artificial language patterns from text. Makes documentation, comments, commit messages, and prose sound more natural and human. Based on Wikipedia's \"Signs of AI writing\" patterns."
}

humanizer skill

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

When to use

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

How to invoke

/humanizer

[paste your text here]

Or ask directly in chat:

Please humanize this text: [your text]

Voice calibration (optional)

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

/humanizer

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

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

What it detects

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

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

Integration with MAPLE

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

Output

Returns a humanized version of your text with:

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

Further reading

提供Jupyter Notebook最佳实践,包括标准单元格结构、使用Papermill实现参数化执行、通过nbformat编程创建笔记本、利用nbconvert导出格式及Git清理输出以确保可复现性。
处理.ipynb文件时 需要标准化Notebook结构 配置参数化执行 生成或导出Notebook
.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实施后自动触发 手动调用/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配置,以及使用dry-run进行验证的工作流,适用于编写或审查K8s清单文件。
编写Kubernetes YAML配置文件 审查K8s部署清单 配置Kustomize多环境覆盖层 设置PodDisruptionBudget或HorizontalPodAutoscaler
.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、部署、状态机)以记录系统架构。要求每功能提供五类图,单图节点不超过30个,确保语法正确并经过Live Editor测试。
需要绘制软件架构图 需要生成数据库ER图 需要展示业务流程时序 需要描述系统部署拓扑 需要建模状态转换逻辑
.opencode/skills/mermaid-diagrams/SKILL.md
npx skills add kinncj/Heimdall --skill mermaid-diagrams -g -y
SKILL.md
Frontmatter
{
    "name": "mermaid-diagrams",
    "description": "Generate and validate Mermaid diagrams (component, sequence, ER, deployment, state machine) for features. Use when documenting architecture."
}

SKILL: Mermaid Diagrams

Required Diagrams Per Feature

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

Rules

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

Component Diagram

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

Sequence Diagram

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

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

ER Diagram

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

State Machine

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

Deployment Diagram

graph TB
    subgraph "Vercel Edge"
        FE["Next.js App"]
        EF["Edge Functions"]
    end
    subgraph "AWS"
        LB["Load Balancer"]
        APP["App Servers"]
        RDS[("RDS PostgreSQL")]
        EC["ElastiCache Redis"]
    end
    FE --> EF
    EF --> LB
    LB --> APP
    APP --> RDS
    APP --> EC
根据已审批的线框图和设计令牌,使用项目指定的UI技术栈(Mantine、Tailwind/shadcn或HTML)生成高保真UI组件Mockup代码。支持Web和Tui目标,包含预检检查以确保线框图状态和令牌文件有效性。
需要基于线框图实现UI组件时 调用mockup技能生成高保真原型时
.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注册表自动安装缺失技能,并在状态文件中记录运行进度以支持实时追踪。
用户需要运行指定的工作流、技能或代理 用户通过/pipeline-runner命令触发任务
.opencode/skills/pipeline-runner/SKILL.md
npx skills add kinncj/Heimdall --skill pipeline-runner -g -y
SKILL.md
Frontmatter
{
    "name": "pipeline-runner",
    "description": "Universal dispatcher: run a named taffy workflow (.claude\/taffy\/<name>.yaml), a skill (\/skill-name), or a sub-agent (@agent-name). Falls back to skills.sh registry when a skill is not found locally. Tracks all runs in .claude\/state\/maple.json so the maple TUI shows live progress."
}

SKILL: pipeline-runner

What It Does

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

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

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

Usage

/pipeline-runner <name>

Examples:

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

List available taffy workflows:

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

List available skills:

ls .claude/skills/

Dispatch Protocol

Step 1: Resolve the target

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

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

Step 2: Initialise state

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

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

Step 2b: Runtime policy enforcement (mandatory)

Before any stage execution, read and enforce:

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

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

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

Step 3a: Taffy workflow execution

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

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

For each stage:

when: guard:

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

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

Dispatch:

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

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

Progress heartbeats (mandatory):

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

Completion artifact gate (mandatory):

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

Step 3b: Skill invocation

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

Step 3c: Agent delegation

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

Step 4: Human-approval gates (taffy only)

When a stage has gate: human-approval:

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

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

Step 5: Completion

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

Output:

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

Failure Handling

After 3 consecutive failures on any stage:

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

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

Session Context

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

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

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

State File Reference

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

.claude/state/maple.json

Field Owner Values
taffy skill workflow/skill/agent name
stage skill current stage name
status skill RUNNING, PAUSED, DONE, FAILED
awaiting_approval skill local MAPLE stage name (e.g., spec-kit = Specification Knowledge & Integration Toolkit) or null — not an external package reference
pipeline skill standard if running 8-phase
started_at skill ISO 8601
updated_at skill ISO 8601
state TUI running or exited
ts TUI ISO 8601

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

.claude/state/approval-pending.txt

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

.claude/state/sessions.json

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

Skip Conditions

  • spike/* and chore/* branches: skip Spec-Kit stages, run implementation stages.
  • Stage when: ui:true on a ui: false story: skip silently, log [pipeline-runner] SKIP stage=<name> reason=ui:false unless quick-launch override is active (force_ui=true, launch_source=maple-x).
提供Playwright CLI交互探索、基于快照的页面操作及会话管理功能。支持将交互过程转化为E2E测试代码,并涵盖测试执行、可视化调试及API测试流程,旨在提升浏览器自动化与测试开发效率。
需要进行交互式浏览器探索 需要基于快照进行页面元素交互 需要将手动操作转换为E2E测试脚本 需要运行或调试Playwright测试套件
.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安全、索引优化及PgBouncer配置,强调幂等性、不修改旧迁移文件及使用CONCURRENTLY创建索引等规则。
编写数据库表结构或迁移脚本 进行PostgreSQL查询性能优化 配置连接池或行级安全策略
.opencode/skills/postgresql-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill postgresql-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "postgresql-patterns",
    "description": "Apply PostgreSQL schema, migration, and query patterns with idempotent SQL. Use when writing database schemas or migrations."
}

SKILL: PostgreSQL Patterns

Migration Naming

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

Table Pattern with RLS

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

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

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

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

-- RLS
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

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

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

Query Optimization

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

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

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

Connection Pooling (PgBouncer)

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

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

Rules

  • NEVER modify existing migration files.
  • Always create new migration for schema changes.
  • Index all foreign keys.
  • Use CONCURRENTLY for index creation on large tables.
  • EXPLAIN ANALYZE all queries > 100ms.
  • Enable RLS on tables with user data.
提供Redis集成最佳实践,涵盖键命名规范、Cache-Aside缓存模式、基于Sorted Set的限流及Session存储实现。强调设置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 技能。提供标准模板,涵盖背景、目标、提案、替代方案及多维影响评估(成本、运维、安全、团队),确保技术选型的透明性与可追溯性。
需要记录重大架构决策时 在多个非平凡技术方案间进行选择时 引入新技术或框架时 变更现有显著模式时
.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 前或合并前的安全审计,识别并报告阻塞性问题。
用户要求执行 /ship-safe 命令 准备打开 PR 或合并功能分支到 main 前
.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格式的用户故事文件。遵循状态机流程,从草稿到人工审批后进入发现阶段。提供文件模板、验证脚本及不同复杂度功能的最小场景数指南,确保规格即代码。
需要为新功能编写验收标准 生成Gherkin用户故事文档 检查故事文件是否已批准
.opencode/skills/spec-kit/SKILL.md
npx skills add kinncj/Heimdall --skill spec-kit -g -y
SKILL.md
Frontmatter
{
    "name": "spec-kit",
    "description": "Write a Gherkin story file to docs\/stories\/ for a feature. The story IS the spec. No intermediate PROBLEM\/SPEC\/PLAN\/TASKS artifacts."
}

SKILL: spec-kit

Purpose

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

State Machine

feature description → story draft → human approval → DISCOVER

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

Story File Location

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

Story File Template

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

## Story

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

## Acceptance Criteria

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

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

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

Definition of Done

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

## Validate a Story File

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

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

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

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

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

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

Check All Stories Are Approved

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

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

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

exit $FAIL

Scenario Count Guidelines

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

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

Skip Conditions

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

Failure Modes

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

Logging

[spec-kit] WRITE   docs/stories/user-auth-reset-password.md
[spec-kit] HALT    awaiting human approval
[spec-kit] OK      docs/stories/user-auth-reset-password.md  status=approved
[spec-kit] SKIP    spike/* branch — spec-kit not required
用于评估新功能上线前的运维就绪状态。涵盖故障模式分析、可观测性指标(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时 故事文件的Gherkin或验收标准发生变更时
.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 接收与处理 创建 Checkout Session 或管理订阅
.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 数据库表或配置行级安全策略 编写 Supabase 边缘函数(Edge Functions) 配置 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模式与Docker集成测试生命周期管理,确保测试先于实现且质量可控。
需要实现新功能时 执行测试驱动开发任务时
.opencode/skills/tdd-workflow/SKILL.md
npx skills add kinncj/Heimdall --skill tdd-workflow -g -y
SKILL.md
Frontmatter
{
    "name": "tdd-workflow",
    "description": "Drive development with a red-green-refactor TDD cycle, ensuring tests are written before implementation. Use when implementing any new functionality."
}

SKILL: TDD Workflow

The RED → GREEN → REFACTOR Cycle

RED Phase (QA Agent)

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

GREEN Phase (Specialist Agent)

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

REFACTOR Phase (Specialist Agent)

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

ATDD Pattern (Acceptance Test-Driven Development)

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

Integration Test Container Lifecycle

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

# Run integration tests
make test-integration

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

Rules

  • Test file is created BEFORE implementation file.
  • Test name format: "should {expected outcome} when {condition}".
  • Never weaken an assertion to make a test pass.
  • Never mock what you can test with a real dependency (use containers).
  • Implementation agent receives FILE PATH, not requirement text.
  • Gate: test must fail before implementation, pass after.
提供Terraform基础设施即代码的最佳实践,涵盖模块化目录结构、资源定义模式及工作流。强调状态管理、成本标注及安全规则,确保编写规范且可维护的Terraform配置。
编写或重构Terraform模块时 需要遵循IaC最佳实践和规范时 配置AWS数据库等资源时
.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 部署最佳实践,涵盖 vercel.json 配置、Edge/Node 运行时选择、环境变量管理及中间件模式。指导用户通过本地构建和预构建部署优化流程,并强调安全头设置与性能优化策略。
需要部署到 Vercel 平台 配置 Next.js 项目的 vercel.json 选择 Edge 或 Node.js 运行时 管理 Vercel 环境变量 编写 Vercel 中间件
.opencode/skills/vercel-patterns/SKILL.md
npx skills add kinncj/Heimdall --skill vercel-patterns -g -y
SKILL.md
Frontmatter
{
    "name": "vercel-patterns",
    "description": "Apply Vercel deployment, edge function, and environment variable patterns. Use when deploying to Vercel."
}

SKILL: Vercel Patterns

vercel.json Configuration

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

Runtime Selection

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

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

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

Environment Variables

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

# Add env var
vercel env add SECRET_KEY production

# List env vars
vercel env ls

Deployment Workflow

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

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

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

Middleware Pattern

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

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

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

Rules

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

SKILL: visual-identity

Purpose

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

Inputs

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

Outputs

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

Target awareness

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

Palette Structure

docs/design/identity/palette.json:

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

WCAG Contrast Check

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

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

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

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

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

Typography Structure

docs/design/identity/typography.json:

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

Spacing + Radius Scales

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

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

Write palette.json

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

Approval Gate

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

Add to the top of palette.json:

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

Check in design-tokens skill:

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

Failure Modes

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

Logging

[visual-identity] PALETTE    docs/design/identity/palette.json  contrast=all-pass
[visual-identity] TYPOGRAPHY docs/design/identity/typography.json
[visual-identity] SKIP       docs/design/identity/palette.json  (approved — locked)
[visual-identity] CONTRAST_FAIL  #2563eb on #ffffff  ratio=3.8  required=4.5
根据用户故事文件生成低保真线框图(ASCII、SVG或HTML)。支持Web和TUI目标,通过设计目标自动匹配输出格式,需人工审批后方可进入下游Mockup工作。
需要为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

ホーム - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-22 08:19
浙ICP备14020137号-1 $お客様$