feat: add recipe-diagrams, worktrees, and misc skills

This commit is contained in:
2026-08-28 13:48:19 -04:00
parent a9669c28ec
commit 103bc141dc
12 changed files with 1384 additions and 0 deletions
+5
View File
@@ -15,6 +15,7 @@ Skills are grouped by invocation type. [User-invoked](docs/invocation.md) skills
- [implement-isolation-tmux](skills/engineering/implement-isolation-tmux/SKILL.md) — Dispatch an isolated worktree agent to implement work from a PRD or issues. - [implement-isolation-tmux](skills/engineering/implement-isolation-tmux/SKILL.md) — Dispatch an isolated worktree agent to implement work from a PRD or issues.
- [improve-codebase-architecture](skills/engineering/improve-codebase-architecture/SKILL.md) — Find and work through opportunities to deepen a codebase's architecture. - [improve-codebase-architecture](skills/engineering/improve-codebase-architecture/SKILL.md) — Find and work through opportunities to deepen a codebase's architecture.
- [project-context-pack](skills/engineering/project-context-pack/SKILL.md) — Build a bounded project context pack for later agent work. - [project-context-pack](skills/engineering/project-context-pack/SKILL.md) — Build a bounded project context pack for later agent work.
- [recipe-diagrams](skills/engineering/recipe-diagrams/SKILL.md) — Convert recipes into high-resolution process-flow diagrams.
- [setup-skills](skills/setup-skills/SKILL.md) — Configure engineering skills, issue tracking, triage labels, and domain docs. - [setup-skills](skills/setup-skills/SKILL.md) — Configure engineering skills, issue tracking, triage labels, and domain docs.
- [to-spec](skills/engineering/to-spec/SKILL.md) — Turn the current conversation into a spec and publish it to the issue tracker. - [to-spec](skills/engineering/to-spec/SKILL.md) — Turn the current conversation into a spec and publish it to the issue tracker.
- [to-tickets](skills/engineering/to-tickets/SKILL.md) — Break a plan or spec into tracer-bullet tickets with dependencies. - [to-tickets](skills/engineering/to-tickets/SKILL.md) — Break a plan or spec into tracer-bullet tickets with dependencies.
@@ -29,7 +30,9 @@ Skills are grouped by invocation type. [User-invoked](docs/invocation.md) skills
### Miscellaneous ### Miscellaneous
- [bro](skills/misc/bro/SKILL.md) — Restate the last message in plain human language. - [bro](skills/misc/bro/SKILL.md) — Restate the last message in plain human language.
- [show-me](skills/misc/show-me/SKILL.md) — Help explain topics visually with concise diagrams and artifacts.
- [tmux-launch-agent](skills/misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window. - [tmux-launch-agent](skills/misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window.
- [visual-verification](skills/misc/visual-verification/SKILL.md) — Verify running desktop UI changes with screenshots and recordings.
### PKM ### PKM
@@ -74,6 +77,8 @@ No user-invoked skills.
- [resolving-merge-conflicts](skills/engineering/resolving-merge-conflicts/SKILL.md) — Resolve an in-progress Git merge or rebase conflict. - [resolving-merge-conflicts](skills/engineering/resolving-merge-conflicts/SKILL.md) — Resolve an in-progress Git merge or rebase conflict.
- [tdd](skills/engineering/tdd/SKILL.md) — Use test-driven development for features, bugs, and integration tests. - [tdd](skills/engineering/tdd/SKILL.md) — Use test-driven development for features, bugs, and integration tests.
- [wizard](skills/engineering/wizard/SKILL.md) — Generate an interactive wizard for steps only a human can perform. - [wizard](skills/engineering/wizard/SKILL.md) — Generate an interactive wizard for steps only a human can perform.
- [worktrees](skills/engineering/worktrees/SKILL.md) — Manage Git worktrees in a canonical repository root.
- [write-discoverable-code](skills/engineering/write-discoverable-code/SKILL.md) — Write code agents and humans can find through plain-text search.
### Miscellaneous ### Miscellaneous
+3
View File
@@ -11,6 +11,7 @@ Daily code work.
- [implement-isolation-tmux](implement-isolation-tmux/SKILL.md) — Dispatch an isolated worktree agent to implement work from a PRD or issues. - [implement-isolation-tmux](implement-isolation-tmux/SKILL.md) — Dispatch an isolated worktree agent to implement work from a PRD or issues.
- [improve-codebase-architecture](improve-codebase-architecture/SKILL.md) — Find and work through opportunities to deepen a codebase's architecture. - [improve-codebase-architecture](improve-codebase-architecture/SKILL.md) — Find and work through opportunities to deepen a codebase's architecture.
- [project-context-pack](project-context-pack/SKILL.md) — Build a bounded project context pack for later agent work. - [project-context-pack](project-context-pack/SKILL.md) — Build a bounded project context pack for later agent work.
- [recipe-diagrams](recipe-diagrams/SKILL.md) — Convert recipes into high-resolution process-flow diagrams.
- [setup-skills](../setup-skills/SKILL.md) — Configure engineering skills, issue tracking, triage labels, and domain docs. - [setup-skills](../setup-skills/SKILL.md) — Configure engineering skills, issue tracking, triage labels, and domain docs.
- [to-spec](to-spec/SKILL.md) — Turn the current conversation into a spec and publish it to the issue tracker. - [to-spec](to-spec/SKILL.md) — Turn the current conversation into a spec and publish it to the issue tracker.
- [to-tickets](to-tickets/SKILL.md) — Break a plan or spec into tracer-bullet tickets with dependencies. - [to-tickets](to-tickets/SKILL.md) — Break a plan or spec into tracer-bullet tickets with dependencies.
@@ -29,3 +30,5 @@ Daily code work.
- [resolving-merge-conflicts](resolving-merge-conflicts/SKILL.md) — Resolve an in-progress Git merge or rebase conflict. - [resolving-merge-conflicts](resolving-merge-conflicts/SKILL.md) — Resolve an in-progress Git merge or rebase conflict.
- [tdd](tdd/SKILL.md) — Use test-driven development for features, bugs, and integration tests. - [tdd](tdd/SKILL.md) — Use test-driven development for features, bugs, and integration tests.
- [wizard](wizard/SKILL.md) — Generate an interactive wizard for steps only a human can perform. - [wizard](wizard/SKILL.md) — Generate an interactive wizard for steps only a human can perform.
- [worktrees](worktrees/SKILL.md) — Manage Git worktrees in a canonical repository root.
- [write-discoverable-code](write-discoverable-code/SKILL.md) — Write code agents and humans can find through plain-text search.
@@ -0,0 +1,62 @@
---
name: recipe-diagrams
description: "Recipe diagrams: convert any recipe into a high-resolution Cooking for Engineers-style PNG process-flow table with aligned ingredient streams, preparation branches, joins, temperatures, timings, and finish steps. Use when the user asks for a recipe diagram."
compatibility: Requires Python 3 and ImageMagick.
disable-model-invocation: true
---
# Recipe diagrams
Convert the recipe into a dependency graph, then render that graph as a high-resolution Cooking for Engineers-style PNG table. Read the table left to right: ingredient rows are streams, columns are stages, and vertically merged action cells are joins.
## Steps
1. **Normalize the source.** Read the complete recipe, including yield, ingredient headings, ingredient preparation, numbered method, notes that alter execution, and any linked source the user supplied. Preserve quantities, equipment, temperatures, times, sensory completion cues, resting/cooling, and serving steps. The source is normalized when every execution-relevant source statement has one prospective home in the diagram.
2. **Build the dependency graph.** Separate global setup from ingredient streams and operations. Treat an ingredient's inline preparation (for example, `onion, diced`) as an operation unless it is purchased in that state. Split an ingredient into labeled portions when the source uses it at different stages. Keep independent preparations in the same column when order does not matter; place dependent operations in later columns. The graph is complete when every ingredient reaches every operation that consumes it and all operations lead to the finished result.
3. **Make the graph planar.** Order ingredient rows so every operation consumes one contiguous row range. Each later join spans the complete ranges of the intermediates it combines. Add columns until actions in one column have disjoint row ranges. The layout is complete when no action range is discontinuous and no two action ranges overlap within a column.
4. **Encode the layout.** Write a temporary JSON file using the schema below. Keep labels imperative and compact, but retain execution details. Use plain strings; the renderer transliterates symbols to ASCII.
```json
{
"title": "Recipe name",
"yield": "about 10 servings",
"setup": [
"Butter and flour a loaf pan",
"Preheat oven to 350 deg F (170 deg C)"
],
"ingredients": [
"2 large (250 g) ripe bananas",
"6 Tbsp (90 mL) butter"
],
"columns": [
{
"actions": [
{ "rows": [0, 0], "label": "mash" },
{ "rows": [1, 1], "label": "melt" }
]
},
{
"actions": [
{ "rows": [0, 1], "label": "mix until smooth" }
]
}
]
}
```
`rows` is an inclusive, zero-based ingredient-row range. A blank stage cell means that stream carries forward unchanged. A cell spanning several rows consumes those ingredients or the intermediates already produced from them. Put pan preparation, preheating, and other recipe-wide prerequisites in `setup`; put cooking, cooling, garnishing, and serving in action columns at their actual dependency point.
5. **Audit before rendering.** Compare the JSON against the source. Verify every ingredient and portion, every operation, all ordering constraints, and all execution details exactly once. Preserve genuine alternatives in the relevant label. Mark source uncertainty with `[?]` and explain it after the diagram rather than inventing a resolution. The audit is complete only when every source item is accounted for.
6. **Render, inspect, and return.** Resolve `scripts/render_recipe_diagram_png.py` relative to this `SKILL.md`, then run:
```bash
python3 scripts/render_recipe_diagram_png.py /tmp/recipe-diagram.json /tmp/recipe-diagram.png --width 3840
```
The renderer creates a 4K-wide PNG with an adaptive height, a local monospaced font, graphical borders, wrapped labels, and true vertically merged action cells. Set `RECIPE_DIAGRAM_FONT` to a `.ttf` or `.ttc` file to override the detected font. If the table is unusually dense, increase `--width`; do not alter the dependency graph merely to fit a chat viewport.
Open the generated PNG and inspect it before returning. Verify that the complete outer border is visible, text stays inside its cells, no labels overlap or clip, joins and row boundaries are unambiguous, and the image remains legible when scaled down. Return the PNG as an image attachment or clear file link rather than pasting an ASCII table. If `[?]` appears, follow the image with a short `Uncertainties` list. The output is complete when rasterization succeeds, the image is at least 3840 pixels wide, and visual inspection confirms crisp text and borders.
@@ -0,0 +1,406 @@
#!/usr/bin/env python3
"""Render a Cooking for Engineers-style recipe dependency graph as aligned ASCII."""
from __future__ import annotations
import argparse
import json
import sys
import textwrap
import unicodedata
from dataclasses import dataclass
from pathlib import Path
from typing import Any
ASCII_REPLACEMENTS = {
"°": " deg ",
"×": "x",
"–": "-",
"—": "-",
"−": "-",
"’": "'",
"‘": "'",
"“": '"',
"”": '"',
"¼": "1/4",
"½": "1/2",
"¾": "3/4",
"⅓": "1/3",
"⅔": "2/3",
"⅛": "1/8",
"⅜": "3/8",
"⅝": "5/8",
"⅞": "7/8",
}
class RecipeDiagramInputError(ValueError):
"""Reports malformed recipe diagram JSON with a searchable error prefix."""
@dataclass(frozen=True)
class RecipeAction:
"""An operation consuming one inclusive, contiguous range of ingredient rows."""
start_row: int
end_row: int
label: str
@dataclass(frozen=True)
class RecipeDiagram:
"""The validated recipe process-flow table consumed by the ASCII renderer."""
title: str
recipe_yield: str
setup: tuple[str, ...]
ingredients: tuple[str, ...]
columns: tuple[tuple[RecipeAction, ...], ...]
@dataclass(frozen=True)
class RecipeDiagramLayout:
"""Wrapped labels, row heights, and fixed column widths for one rendering."""
column_widths: tuple[int, ...]
row_heights: tuple[int, ...]
ingredient_lines: tuple[tuple[str, ...], ...]
action_lines: tuple[dict[RecipeAction, tuple[str, ...]], ...]
def ascii_recipe_text(value: str) -> str:
"""Transliterate recipe text so every rendered character is seven-bit ASCII."""
replaced = "".join(ASCII_REPLACEMENTS.get(character, character) for character in value)
normalized = unicodedata.normalize("NFKD", replaced)
ascii_text = normalized.encode("ascii", "ignore").decode("ascii")
return " ".join(ascii_text.split())
def require_recipe_string(value: Any, field_name: str, allow_empty: bool = False) -> str:
"""Validate and normalize one string field from recipe diagram JSON."""
if not isinstance(value, str):
raise RecipeDiagramInputError(f"{field_name} must be a string")
normalized = ascii_recipe_text(value)
if not allow_empty and not normalized:
raise RecipeDiagramInputError(f"{field_name} must not be empty")
return normalized
def parse_recipe_diagram(document: Any) -> RecipeDiagram:
"""Parse and validate the complete recipe diagram JSON document."""
if not isinstance(document, dict):
raise RecipeDiagramInputError("top-level JSON value must be an object")
title = require_recipe_string(document.get("title"), "title")
recipe_yield = require_recipe_string(document.get("yield", ""), "yield", allow_empty=True)
setup_value = document.get("setup", [])
if not isinstance(setup_value, list):
raise RecipeDiagramInputError("setup must be an array of strings")
setup = tuple(
require_recipe_string(item, f"setup[{index}]")
for index, item in enumerate(setup_value)
)
ingredients_value = document.get("ingredients")
if not isinstance(ingredients_value, list) or not ingredients_value:
raise RecipeDiagramInputError("ingredients must be a non-empty array of strings")
ingredients = tuple(
require_recipe_string(item, f"ingredients[{index}]")
for index, item in enumerate(ingredients_value)
)
columns_value = document.get("columns")
if not isinstance(columns_value, list) or not columns_value:
raise RecipeDiagramInputError("columns must be a non-empty array")
columns: list[tuple[RecipeAction, ...]] = []
for column_index, column_value in enumerate(columns_value):
if not isinstance(column_value, dict):
raise RecipeDiagramInputError(f"columns[{column_index}] must be an object")
actions_value = column_value.get("actions", [])
if not isinstance(actions_value, list):
raise RecipeDiagramInputError(f"columns[{column_index}].actions must be an array")
actions: list[RecipeAction] = []
occupied_rows: set[int] = set()
for action_index, action_value in enumerate(actions_value):
action_field = f"columns[{column_index}].actions[{action_index}]"
if not isinstance(action_value, dict):
raise RecipeDiagramInputError(f"{action_field} must be an object")
rows_value = action_value.get("rows")
if (
not isinstance(rows_value, list)
or len(rows_value) != 2
or any(isinstance(row, bool) or not isinstance(row, int) for row in rows_value)
):
raise RecipeDiagramInputError(f"{action_field}.rows must contain two integers")
start_row, end_row = rows_value
if start_row < 0 or end_row < start_row or end_row >= len(ingredients):
raise RecipeDiagramInputError(
f"{action_field}.rows must be an inclusive range within 0..{len(ingredients) - 1}"
)
action_rows = set(range(start_row, end_row + 1))
if occupied_rows.intersection(action_rows):
raise RecipeDiagramInputError(
f"{action_field}.rows overlaps another action in column {column_index}"
)
occupied_rows.update(action_rows)
actions.append(
RecipeAction(
start_row=start_row,
end_row=end_row,
label=require_recipe_string(action_value.get("label"), f"{action_field}.label"),
)
)
columns.append(tuple(sorted(actions, key=lambda action: action.start_row)))
return RecipeDiagram(
title=title,
recipe_yield=recipe_yield,
setup=setup,
ingredients=ingredients,
columns=tuple(columns),
)
def calculate_column_widths(total_width: int, process_column_count: int) -> tuple[int, ...]:
"""Allocate one ingredient width and equal process widths within total output width."""
table_column_count = process_column_count + 1
content_width = total_width - table_column_count - 1
minimum_ingredient_width = 24
minimum_process_width = 10
minimum_content_width = minimum_ingredient_width + minimum_process_width * process_column_count
if content_width < minimum_content_width:
minimum_total_width = minimum_content_width + table_column_count + 1
raise RecipeDiagramInputError(
f"diagram width {total_width} is too narrow; use --width {minimum_total_width} or greater"
)
ingredient_width = min(44, max(minimum_ingredient_width, int(content_width * 0.38)))
remaining_width = content_width - ingredient_width
process_width, extra_width = divmod(remaining_width, process_column_count)
widths = [ingredient_width]
widths.extend(
process_width + (1 if index < extra_width else 0)
for index in range(process_column_count)
)
return tuple(widths)
def wrap_recipe_label(label: str, width: int) -> tuple[str, ...]:
"""Wrap one ASCII label without breaking words unless a word exceeds the cell width."""
wrapped = textwrap.wrap(
label,
width=width,
break_long_words=True,
break_on_hyphens=False,
replace_whitespace=True,
drop_whitespace=True,
)
return tuple(wrapped or [""])
def build_recipe_layout(diagram: RecipeDiagram, total_width: int) -> RecipeDiagramLayout:
"""Compute wrapped cell content and enough row height for every merged action."""
column_widths = calculate_column_widths(total_width, len(diagram.columns))
ingredient_lines = tuple(
wrap_recipe_label(ingredient, column_widths[0]) for ingredient in diagram.ingredients
)
row_heights = [len(lines) for lines in ingredient_lines]
action_lines: list[dict[RecipeAction, tuple[str, ...]]] = []
for column_index, actions in enumerate(diagram.columns):
process_width = column_widths[column_index + 1]
wrapped_actions: dict[RecipeAction, tuple[str, ...]] = {}
for action in actions:
lines = wrap_recipe_label(action.label, process_width)
wrapped_actions[action] = lines
available_height = sum(row_heights[action.start_row : action.end_row + 1])
if len(lines) > available_height:
row_heights[action.end_row] += len(lines) - available_height
action_lines.append(wrapped_actions)
return RecipeDiagramLayout(
column_widths=column_widths,
row_heights=tuple(row_heights),
ingredient_lines=ingredient_lines,
action_lines=tuple(action_lines),
)
def find_row_action(actions: tuple[RecipeAction, ...], row_index: int) -> RecipeAction | None:
"""Find the merged action occupying one process-column row, if present."""
for action in actions:
if action.start_row <= row_index <= action.end_row:
return action
return None
def center_cell_text(text: str, width: int) -> str:
"""Center text in one fixed-width ASCII table cell."""
return text.center(width)
def render_horizontal_border(widths: tuple[int, ...], fill: str = "-") -> str:
"""Render a full table border using one ASCII fill character."""
return "+" + "+".join(fill * width for width in widths) + "+"
def render_merged_separator(
diagram: RecipeDiagram,
layout: RecipeDiagramLayout,
boundary_row: int,
) -> str:
"""Render a row boundary while leaving active row-spanning action cells open."""
segments = ["-" * layout.column_widths[0]]
for column_index, actions in enumerate(diagram.columns):
spanning_boundary = any(
action.start_row < boundary_row <= action.end_row for action in actions
)
fill = " " if spanning_boundary else "-"
segments.append(fill * layout.column_widths[column_index + 1])
return "+" + "+".join(segments) + "+"
def action_line_for_row(
action: RecipeAction,
wrapped_lines: tuple[str, ...],
row_index: int,
line_index: int,
row_heights: tuple[int, ...],
) -> str:
"""Place a merged action label at the vertical center of its complete row range."""
total_height = sum(row_heights[action.start_row : action.end_row + 1])
top_padding = (total_height - len(wrapped_lines)) // 2
lines_before_row = sum(row_heights[action.start_row:row_index])
merged_line_index = lines_before_row + line_index
label_line_index = merged_line_index - top_padding
if 0 <= label_line_index < len(wrapped_lines):
return wrapped_lines[label_line_index]
return ""
def render_recipe_diagram(diagram: RecipeDiagram, total_width: int) -> str:
"""Render and internally verify one complete aligned ASCII recipe diagram."""
layout = build_recipe_layout(diagram, total_width)
widths = layout.column_widths
lines: list[str] = []
title = diagram.title
if diagram.recipe_yield:
title = f"{title} ({diagram.recipe_yield})"
title_lines = wrap_recipe_label(title, total_width - 2)
lines.append(render_horizontal_border(widths))
for title_line in title_lines:
lines.append("|" + center_cell_text(title_line, total_width - 2) + "|")
lines.append(render_horizontal_border(widths))
for setup_line in diagram.setup:
for wrapped_setup_line in wrap_recipe_label(setup_line, total_width - 2):
lines.append("|" + center_cell_text(wrapped_setup_line, total_width - 2) + "|")
lines.append(render_horizontal_border(widths))
for row_index in range(len(diagram.ingredients)):
ingredient_row_lines = layout.ingredient_lines[row_index]
for line_index in range(layout.row_heights[row_index]):
ingredient_text = (
ingredient_row_lines[line_index]
if line_index < len(ingredient_row_lines)
else ""
)
cells = [ingredient_text.ljust(widths[0])]
for column_index, actions in enumerate(diagram.columns):
action = find_row_action(actions, row_index)
action_text = ""
if action is not None:
action_text = action_line_for_row(
action,
layout.action_lines[column_index][action],
row_index,
line_index,
layout.row_heights,
)
cells.append(center_cell_text(action_text, widths[column_index + 1]))
lines.append("|" + "|".join(cells) + "|")
if row_index < len(diagram.ingredients) - 1:
lines.append(render_merged_separator(diagram, layout, row_index + 1))
lines.append(render_horizontal_border(widths))
validate_rendered_diagram(lines, total_width)
return "\n".join(lines)
def validate_rendered_diagram(lines: list[str], expected_width: int) -> None:
"""Reject renderer output containing non-ASCII characters or misaligned lines."""
for line_number, line in enumerate(lines, start=1):
if len(line) != expected_width:
raise RuntimeError(
f"Recipe diagram renderer error: line {line_number} has width {len(line)}, expected {expected_width}"
)
if not line.isascii():
raise RuntimeError(
f"Recipe diagram renderer error: line {line_number} contains a non-ASCII character"
)
def load_recipe_document(input_path: Path) -> Any:
"""Load recipe diagram JSON from a named file or standard input."""
try:
if str(input_path) == "-":
return json.load(sys.stdin)
with input_path.open("r", encoding="utf-8") as input_file:
return json.load(input_file)
except (OSError, json.JSONDecodeError) as error:
raise RecipeDiagramInputError(f"cannot read {input_path}: {error}") from error
def parse_command_line() -> argparse.Namespace:
"""Parse the recipe diagram renderer command-line arguments."""
parser = argparse.ArgumentParser(
description="Render recipe dependency JSON as an aligned ASCII process-flow table."
)
parser.add_argument("input", type=Path, help="JSON input file, or - for standard input")
parser.add_argument(
"--width",
type=int,
default=120,
help="exact output width in ASCII characters (default: 120)",
)
return parser.parse_args()
def main() -> int:
"""Run the recipe diagram ASCII renderer command-line program."""
arguments = parse_command_line()
try:
document = load_recipe_document(arguments.input)
diagram = parse_recipe_diagram(document)
print(render_recipe_diagram(diagram, arguments.width))
except RecipeDiagramInputError as error:
print(f"Recipe diagram input error: {error}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,390 @@
#!/usr/bin/env python3
"""Render recipe dependency JSON as a high-resolution PNG table."""
from __future__ import annotations
import argparse
import html
import os
import shutil
import subprocess
import sys
import tempfile
import textwrap
from dataclasses import dataclass
from pathlib import Path
from render_recipe_diagram import (
RecipeAction,
RecipeDiagram,
RecipeDiagramInputError,
ascii_recipe_text,
load_recipe_document,
parse_recipe_diagram,
)
@dataclass(frozen=True)
class PngLayout:
width: int
height: int
margin: int
table_x: int
table_width: int
column_widths: tuple[int, ...]
title_height: int
setup_heights: tuple[int, ...]
row_heights: tuple[int, ...]
ingredient_lines: tuple[tuple[str, ...], ...]
setup_lines: tuple[tuple[str, ...], ...]
action_lines: tuple[dict[RecipeAction, tuple[str, ...]], ...]
font_size: int
title_font_size: int
line_height: int
padding: int
def wrap_for_pixels(text: str, pixel_width: int, font_size: int, padding: int) -> tuple[str, ...]:
"""Wrap monospaced text using a conservative character-width estimate."""
usable_width = max(1, pixel_width - 2 * padding)
character_width = font_size * 0.62
character_count = max(1, int(usable_width / character_width))
lines = textwrap.wrap(
ascii_recipe_text(text),
width=character_count,
break_long_words=True,
break_on_hyphens=False,
replace_whitespace=True,
drop_whitespace=True,
)
return tuple(lines or [""])
def build_png_layout(diagram: RecipeDiagram, width: int) -> PngLayout:
"""Calculate a high-density table layout with readable wrapped text."""
if width < 1920:
raise RecipeDiagramInputError("PNG width must be at least 1920 pixels")
scale = width / 3840
margin = round(72 * scale)
padding = max(12, round(18 * scale))
font_size = max(20, round(34 * scale))
title_font_size = max(30, round(50 * scale))
line_height = max(29, round(47 * scale))
table_width = width - 2 * margin
process_count = len(diagram.columns)
ingredient_width = round(table_width * (0.24 if process_count >= 6 else 0.30))
remaining_width = table_width - ingredient_width
process_width, extra = divmod(remaining_width, process_count)
column_widths = (ingredient_width,) + tuple(
process_width + (1 if index < extra else 0) for index in range(process_count)
)
ingredient_lines = tuple(
wrap_for_pixels(ingredient, ingredient_width, font_size, padding)
for ingredient in diagram.ingredients
)
minimum_row_height = max(round(62 * scale), line_height + 2 * padding)
row_heights = [
max(minimum_row_height, len(lines) * line_height + 2 * padding)
for lines in ingredient_lines
]
action_lines: list[dict[RecipeAction, tuple[str, ...]]] = []
for column_index, actions in enumerate(diagram.columns):
wrapped_actions: dict[RecipeAction, tuple[str, ...]] = {}
cell_width = column_widths[column_index + 1]
for action in actions:
lines = wrap_for_pixels(action.label, cell_width, font_size, padding)
wrapped_actions[action] = lines
required_height = len(lines) * line_height + 2 * padding
available_height = sum(row_heights[action.start_row : action.end_row + 1])
if required_height > available_height:
row_heights[action.end_row] += required_height - available_height
action_lines.append(wrapped_actions)
setup_lines = tuple(
wrap_for_pixels(item, table_width, font_size, padding) for item in diagram.setup
)
setup_heights = tuple(len(lines) * line_height + 2 * padding for lines in setup_lines)
title_height = max(round(104 * scale), title_font_size + 2 * padding)
height = 2 * margin + title_height + sum(setup_heights) + sum(row_heights)
return PngLayout(
width=width,
height=height,
margin=margin,
table_x=margin,
table_width=table_width,
column_widths=column_widths,
title_height=title_height,
setup_heights=setup_heights,
row_heights=tuple(row_heights),
ingredient_lines=ingredient_lines,
setup_lines=setup_lines,
action_lines=tuple(action_lines),
font_size=font_size,
title_font_size=title_font_size,
line_height=line_height,
padding=padding,
)
def svg_text_lines(
lines: tuple[str, ...],
x: float,
center_y: float,
font_size: int,
line_height: int,
anchor: str,
weight: int = 500,
color: str = "#303446",
) -> str:
"""Create vertically centered SVG text elements for wrapped lines."""
first_baseline = center_y - ((len(lines) - 1) * line_height) / 2 + font_size * 0.35
escaped_anchor = html.escape(anchor, quote=True)
elements = []
for index, line in enumerate(lines):
elements.append(
f'<text x="{x:.1f}" y="{first_baseline + index * line_height:.1f}" '
f'text-anchor="{escaped_anchor}" font-size="{font_size}" font-weight="{weight}" '
f'fill="{color}">{html.escape(line)}</text>'
)
return "\n".join(elements)
def action_spans_boundary(actions: tuple[RecipeAction, ...], boundary_row: int) -> bool:
return any(action.start_row < boundary_row <= action.end_row for action in actions)
def render_svg(diagram: RecipeDiagram, layout: PngLayout) -> str:
"""Render the table as SVG so rasterization retains crisp geometry and text."""
scale = layout.width / 3840
border = max(2, round(3 * scale))
outer_border = max(6, round(10 * scale))
radius = max(8, round(14 * scale))
x_positions = [layout.table_x]
for cell_width in layout.column_widths:
x_positions.append(x_positions[-1] + cell_width)
parts = [
'<?xml version="1.0" encoding="UTF-8"?>',
f'<svg xmlns="http://www.w3.org/2000/svg" width="{layout.width}" height="{layout.height}" '
f'viewBox="0 0 {layout.width} {layout.height}">',
"<style>",
"text { font-family: monospace; }",
".rule { stroke: #51576d; stroke-linecap: square; shape-rendering: geometricPrecision; }",
"</style>",
f'<rect width="{layout.width}" height="{layout.height}" fill="#f7f7fb"/>',
f'<rect x="{layout.table_x}" y="{layout.margin}" width="{layout.table_width}" '
f'height="{layout.height - 2 * layout.margin}" rx="{radius}" fill="#ffffff" stroke="#51576d" stroke-width="{border}"/>',
]
y = layout.margin
title = diagram.title
if diagram.recipe_yield:
title = f"{title} ({diagram.recipe_yield})"
parts.append(
f'<path d="M {layout.table_x + radius} {y} H {layout.table_x + layout.table_width - radius} '
f'Q {layout.table_x + layout.table_width} {y} {layout.table_x + layout.table_width} {y + radius} '
f'V {y + layout.title_height} H {layout.table_x} V {y + radius} '
f'Q {layout.table_x} {y} {layout.table_x + radius} {y} Z" fill="#303446"/>'
)
title_lines = wrap_for_pixels(title, layout.table_width, layout.title_font_size, layout.padding)
parts.append(
svg_text_lines(
title_lines,
layout.table_x + layout.table_width / 2,
y + layout.title_height / 2,
layout.title_font_size,
round(layout.title_font_size * 1.3),
"middle",
weight=700,
color="#ffffff",
)
)
y += layout.title_height
parts.append(
f'<line class="rule" x1="{layout.table_x}" y1="{y}" x2="{layout.table_x + layout.table_width}" y2="{y}" stroke-width="{border}"/>'
)
for lines, setup_height in zip(layout.setup_lines, layout.setup_heights, strict=True):
parts.append(
f'<rect x="{layout.table_x}" y="{y}" width="{layout.table_width}" height="{setup_height}" fill="#e9eaf2"/>'
)
parts.append(
svg_text_lines(
lines,
layout.table_x + layout.table_width / 2,
y + setup_height / 2,
layout.font_size,
layout.line_height,
"middle",
weight=600,
)
)
y += setup_height
parts.append(
f'<line class="rule" x1="{layout.table_x}" y1="{y}" x2="{layout.table_x + layout.table_width}" y2="{y}" stroke-width="{border}"/>'
)
body_y = y
row_tops = [body_y]
for row_height in layout.row_heights:
row_tops.append(row_tops[-1] + row_height)
for row_index, row_height in enumerate(layout.row_heights):
fill = "#fbfbfd" if row_index % 2 == 0 else "#f4f5f9"
parts.append(
f'<rect x="{x_positions[0]}" y="{row_tops[row_index]}" width="{layout.column_widths[0]}" height="{row_height}" fill="{fill}"/>'
)
for column_index, actions in enumerate(diagram.columns):
for action in actions:
action_y = row_tops[action.start_row]
action_height = row_tops[action.end_row + 1] - action_y
parts.append(
f'<rect x="{x_positions[column_index + 1]}" y="{action_y}" '
f'width="{layout.column_widths[column_index + 1]}" height="{action_height}" fill="#f0eef8"/>'
)
for x in x_positions[1:-1]:
parts.append(
f'<line class="rule" x1="{x}" y1="{body_y}" x2="{x}" y2="{row_tops[-1]}" stroke-width="{border}"/>'
)
for boundary_row in range(1, len(diagram.ingredients)):
boundary_y = row_tops[boundary_row]
parts.append(
f'<line class="rule" x1="{x_positions[0]}" y1="{boundary_y}" x2="{x_positions[1]}" y2="{boundary_y}" stroke-width="{border}"/>'
)
for column_index, actions in enumerate(diagram.columns):
if not action_spans_boundary(actions, boundary_row):
parts.append(
f'<line class="rule" x1="{x_positions[column_index + 1]}" y1="{boundary_y}" '
f'x2="{x_positions[column_index + 2]}" y2="{boundary_y}" stroke-width="{border}"/>'
)
for row_index, lines in enumerate(layout.ingredient_lines):
parts.append(
svg_text_lines(
lines,
x_positions[0] + layout.padding,
(row_tops[row_index] + row_tops[row_index + 1]) / 2,
layout.font_size,
layout.line_height,
"start",
weight=600,
)
)
for column_index, actions in enumerate(diagram.columns):
for action in actions:
parts.append(
svg_text_lines(
layout.action_lines[column_index][action],
(x_positions[column_index + 1] + x_positions[column_index + 2]) / 2,
(row_tops[action.start_row] + row_tops[action.end_row + 1]) / 2,
layout.font_size,
layout.line_height,
"middle",
weight=500,
)
)
table_bottom = layout.height - layout.margin
table_right = layout.table_x + layout.table_width
parts.extend(
[
f'<rect x="{layout.table_x}" y="{layout.margin}" width="{layout.table_width}" height="{outer_border}" fill="#303446"/>',
f'<rect x="{layout.table_x}" y="{table_bottom - outer_border}" width="{layout.table_width}" height="{outer_border}" fill="#303446"/>',
f'<rect x="{layout.table_x}" y="{layout.margin}" width="{outer_border}" height="{table_bottom - layout.margin}" fill="#303446"/>',
f'<rect x="{table_right - outer_border}" y="{layout.margin}" width="{outer_border}" height="{table_bottom - layout.margin}" fill="#303446"/>',
]
)
parts.append("</svg>")
return "\n".join(parts)
def resolve_monospace_font() -> Path:
"""Find a crisp local monospace font for ImageMagick's SVG renderer."""
configured_font = os.environ.get("RECIPE_DIAGRAM_FONT")
candidates = [
configured_font,
"/System/Library/Fonts/SFNSMono.ttf",
"/System/Library/Fonts/Menlo.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/liberation2/LiberationMono-Regular.ttf",
]
for candidate in candidates:
if candidate and Path(candidate).is_file():
return Path(candidate)
raise RecipeDiagramInputError(
"no monospace font found; set RECIPE_DIAGRAM_FONT to a .ttf or .ttc file"
)
def rasterize_svg(svg: str, output_path: Path) -> None:
"""Rasterize SVG to PNG with ImageMagick."""
magick = shutil.which("magick")
if magick is None:
raise RecipeDiagramInputError("ImageMagick is required; install the `imagemagick` package")
font_path = resolve_monospace_font()
output_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", suffix=".svg", encoding="utf-8", delete=False) as svg_file:
svg_file.write(svg)
svg_path = Path(svg_file.name)
try:
result = subprocess.run(
[
magick,
"-font",
str(font_path),
str(svg_path),
"-strip",
"-define",
"png:color-type=6",
str(output_path),
],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
message = result.stderr.strip() or result.stdout.strip() or "unknown ImageMagick error"
raise RecipeDiagramInputError(f"cannot rasterize PNG: {message}")
finally:
svg_path.unlink(missing_ok=True)
def parse_command_line() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Render recipe dependency JSON as a high-resolution PNG")
parser.add_argument("input", type=Path, help="JSON input file")
parser.add_argument("output", type=Path, help="PNG output path")
parser.add_argument("--width", type=int, default=3840, help="PNG width in pixels (default: 3840)")
return parser.parse_args()
def main() -> int:
arguments = parse_command_line()
try:
diagram = parse_recipe_diagram(load_recipe_document(arguments.input))
layout = build_png_layout(diagram, arguments.width)
rasterize_svg(render_svg(diagram, layout), arguments.output)
print(f"Rendered {arguments.output} ({layout.width}x{layout.height})")
except RecipeDiagramInputError as error:
print(f"Recipe diagram PNG input error: {error}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
+64
View File
@@ -0,0 +1,64 @@
---
name: worktrees
description: Manage Git worktrees in a canonical `.bare` repository root. Use when creating, reusing, listing, removing, or repairing worktrees, or when setting up a repository to keep all branch checkouts under one root.
---
# Git worktrees
Use one root per repository:
```text
<repo>/
.git # gitdir: ./.bare
.bare/ # shared repository and object store
main/ # linked worktree
<topic>/ # linked worktree
```
Run repository-wide commands from `<repo>` and development commands from a linked worktree. Keep every checkout under `<repo>`.
## Create or reuse a worktree
1. From any linked worktree or the canonical root, run [`scripts/new-worktree.sh`](scripts/new-worktree.sh):
```bash
scripts/new-worktree.sh <local-dir> <branch> [base]
```
Set `WORKTREE_ROOT` only when root discovery is unavailable. Set `WORKTREE_REMOTE` when the remote is not `origin`.
2. Enter `<repo>/<local-dir>` and run:
```bash
git status --short --branch
```
Creation is complete when `git worktree list` contains the path and its expected branch, and status is clean unless the branch already carried changes.
The helper fetches and prunes, reuses an existing local branch, tracks a matching remote branch, or creates a new branch from `[base]`. Run it with `--help` for the exact decision order and defaults.
## Remove a worktree
1. Account for every staged, unstaged, and untracked change:
```bash
git -C <repo>/<local-dir> status --short --branch
```
2. Remove the checkout through Git:
```bash
git -C <repo> worktree remove <local-dir>
```
3. Delete the local branch only when its commits are integrated or intentionally discarded:
```bash
git -C <repo> branch -d <branch>
```
Removal is complete when the path is absent from both the filesystem and `git -C <repo> worktree list`.
## Setup and recovery
Read [`references/canonical-root.md`](references/canonical-root.md) when converting a repository to this layout, validating its invariants, cleaning stale registrations, or repairing moved worktrees.
@@ -0,0 +1,57 @@
# Canonical worktree root
## Create the root
For a new local root at `<repo>`:
```bash
mkdir <repo>
cd <repo>
git clone --bare <url> .bare
printf '%s\n' 'gitdir: ./.bare' >.git
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
git config core.logAllRefUpdates true
git config worktree.useRelativePaths true
git fetch --prune origin
git remote set-head origin --auto
git worktree add main main
git -C main branch --set-upstream-to=origin/main main
```
Replace `main` in the final two commands when the remote default branch has another name. Setup is complete when the invariants below hold and `main` is listed by `git worktree list`.
For an existing clone with unpublished state, preserve its branches, tags, reflogs, worktree changes, ignored files, hooks, and repository-local configuration before conversion. Prefer creating a fresh canonical root and moving commits through Git over rearranging live metadata in place.
## Invariants
Run from `<repo>`:
```bash
git rev-parse --is-bare-repository
git config --get remote.origin.fetch
git config --get core.logAllRefUpdates
git config --get worktree.useRelativePaths
git worktree list --verbose
```
A canonical root resolves to a bare repository, fetches remote branches into `refs/remotes/origin/*`, keeps reflogs, uses relative worktree paths, and lists every live checkout beneath the root.
## Stale registrations
Inspect before pruning:
```bash
git worktree prune --dry-run --verbose
```
Run `git worktree prune --verbose` only after every reported registration is confirmed stale.
## Moved roots or worktrees
After moving the root or a linked worktree, run:
```bash
git worktree repair
```
Then verify every path with `git worktree list --verbose` and `git -C <path> status --short --branch`. Recovery is complete when each live path resolves to its expected branch and no valid registration appears in a prune dry run.
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'USAGE'
Usage: new-worktree.sh <local-dir> <branch> [base]
Create a linked worktree under a canonical repository root:
<repo>/.git -> gitdir: ./.bare
<repo>/.bare -> shared bare repository
<repo>/<name> -> linked worktree
Branch selection:
1. Reuse <branch> when it exists locally.
2. Track WORKTREE_REMOTE/<branch> when it exists remotely.
3. Create <branch> from [base].
The default remote is origin. The default base is the remote's default branch,
then main or master when either exists locally. Set WORKTREE_ROOT to override
root discovery and WORKTREE_REMOTE to select another remote.
USAGE
}
if [[ ${1:-} == "-h" || ${1:-} == "--help" ]]; then
usage
exit 0
fi
if [[ $# -lt 2 || $# -gt 3 ]]; then
usage
exit 2
fi
local_dir=$1
branch=$2
base=${3:-}
if [[ -z "$local_dir" || "$local_dir" == "." || "$local_dir" == ".." || "$local_dir" == */* ]]; then
echo "new-worktree: local-dir must name one direct child of the repository root: $local_dir" >&2
exit 2
fi
if ! git check-ref-format --branch "$branch" >/dev/null 2>&1; then
echo "new-worktree: invalid branch name: $branch" >&2
exit 2
fi
if [[ -n ${WORKTREE_ROOT:-} ]]; then
root=$WORKTREE_ROOT
else
if ! common_dir=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null); then
echo "new-worktree: run from a canonical repository root or one of its linked worktrees" >&2
exit 1
fi
if [[ ${common_dir##*/} != ".bare" ]]; then
echo "new-worktree: shared Git directory is not a canonical .bare directory: $common_dir" >&2
echo "new-worktree: set WORKTREE_ROOT or convert the repository to the canonical layout" >&2
exit 1
fi
root=${common_dir%/.bare}
fi
if [[ $(git -C "$root" rev-parse --is-bare-repository 2>/dev/null) != "true" ]]; then
echo "new-worktree: repository root does not resolve to a bare repository: $root" >&2
exit 1
fi
if [[ -e "$root/$local_dir" ]]; then
echo "new-worktree: destination already exists: $root/$local_dir" >&2
exit 1
fi
remote=${WORKTREE_REMOTE:-origin}
if git -C "$root" remote get-url "$remote" >/dev/null 2>&1; then
echo "Fetching $remote..." >&2
git -C "$root" fetch --prune "$remote"
has_remote=true
else
has_remote=false
if [[ -n ${WORKTREE_REMOTE:-} ]]; then
echo "new-worktree: remote does not exist: $remote" >&2
exit 1
fi
fi
if git -C "$root" show-ref --verify --quiet "refs/heads/$branch"; then
echo "Adding existing local branch '$branch' at $root/$local_dir" >&2
git -C "$root" worktree add -- "$local_dir" "$branch"
elif [[ $has_remote == true ]] && git -C "$root" show-ref --verify --quiet "refs/remotes/$remote/$branch"; then
echo "Creating tracking branch '$branch' from $remote/$branch at $root/$local_dir" >&2
git -C "$root" worktree add --track -b "$branch" -- "$local_dir" "$remote/$branch"
else
if [[ -z "$base" && $has_remote == true ]]; then
base=$(git -C "$root" symbolic-ref --quiet --short "refs/remotes/$remote/HEAD" 2>/dev/null || true)
fi
if [[ -z "$base" ]]; then
if git -C "$root" show-ref --verify --quiet refs/heads/main; then
base=main
elif git -C "$root" show-ref --verify --quiet refs/heads/master; then
base=master
else
echo "new-worktree: no default base found; pass [base] explicitly" >&2
exit 1
fi
fi
if ! git -C "$root" rev-parse --verify --quiet "$base^{commit}" >/dev/null; then
echo "new-worktree: base does not resolve to a commit: $base" >&2
exit 1
fi
echo "Creating branch '$branch' from $base at $root/$local_dir" >&2
git -C "$root" worktree add --no-track -b "$branch" -- "$local_dir" "$base"
fi
echo >&2
git -C "$root" worktree list --verbose
@@ -0,0 +1,107 @@
---
name: write-discoverable-code
description: |
Rules for writing code that coding agents (and humans) can find and understand through
plain-text search. Apply whenever writing or renaming code: functions, types, constants,
files, error messages, doc comments.
Grounded in measurement: agents navigate by plain-text search, not by AST or
language server, so every identifier is a search query and every search miss
costs wasted reads.
license: MIT
---
# Write discoverable code
Coding agents discover code by searching for strings and reading small windows around the
hits. They have no hover text, no jump-to-definition, and no memory between sessions. These
rules make code resolvable in one search instead of five.
## 1. Names are search queries
- **Exported symbols get 2–4 word names, at least one of them a domain word.**
`diffUserObjects`, not `diff`. `queueEventForDispatch`, not `queue`.
Measured on a ~700k-line monorepo: 1-word exported names are globally unique 61% of
the time; 3-word names 96%; 4+ words 98%. Three words is the knee of the curve.
Use the shortest name that greps uniquely; put the rest in the doc comment.
- **Give generic verbs their object.** `sanitizeEmailHtml`, not `sanitize`;
`validateSmtpConfig`, not `validateConfig`. Qualify only as far as uniqueness
requires, then stop.
- **One definition site per symbol.** Never copy a function between files; move it and
delete the original in the same change. Shared helpers get one concept-named home
and are imported everywhere else.
- **Do not rely on the module path to disambiguate a generic name.** The import that
disambiguates `users/diff.ts` from `orders/diff.ts` sits at the top of the file; the
search hit is at line 300. Put the context in the symbol (`formatDurationMs`), not the
folder. Exception: rigid, absolute conventions where the path carries the meaning
(e.g. every contract file exporting `Input`/`Output`).
- **One concept, one spelling.** Pick `organizationId` or `orgId` and use it everywhere;
every synonym splits every future search in half. Reuse existing vocabulary in the
codebase you are editing rather than introducing near-synonyms.
- **When behavior or audience changes, rename in the same commit.** A stale name is
misinformation with a 100% open rate — that includes visibility markers: a `_private`
helper that other modules now import needs a public name.
- **Filenames are names too — never use bare-role filenames.** `config.ts`, `types.ts`,
`utils.ts`, `helpers.ts`, `handlers.ts` say nothing in a search result and collide with
every other module's config/types/utils in the repo. Prefix the domain:
`billing-plan-config.ts`, not `config.ts`. (`index.ts` is acceptable only as a
thin re-export entry point.)
## 2. Types are the documentation agents can't skip
- **Brand your primitive IDs.** `z.string().brand<'UserId'>()` (TS) or newtypes (Rust).
A `transferOwnership(userId: string, orgId: string)` signature makes argument
transposition invisible; branded types make it a compile error that names the concepts.
- **Use capability-token parameter types** for privileged operations (e.g. requiring an
`OrgScopedDb` instead of a raw connection). A comment is a request; a required type is
physics.
- **Model state with discriminated unions**, not clusters of nullable fields with implicit
rules.
- **Name types like they'll be quoted back** — they will be, in compiler errors the agent
uses to self-correct. `OrgScopedDb` explains itself; `Ctx2` does not. Avoid `any`: every
`any` is a spot where the compiler goes silent and the agent is back to guessing.
## 3. Say it where the search lands
- **One-line doc comment on every export**, stating the sharpest constraint the code
itself can't show (units, timezone, "source time, not insert time", ownership).
The definition is where a name search lands; that line is your whole message.
- **Write the plain-words phrase in the doc comment.** Searches arrive as natural language
("rate limit", "retry delay"), and camelCase identifiers don't match phrase greps —
`RateLimiter` is invisible to a search for "rate limit". The doc comment above each
export should contain, in ordinary spaced-out words, the phrase someone would search
for: a `SessionExpiryChecker` should say /\*_ Checks whether the user session has
expired. _/ so that a grep for "session expired" or "session has expired" lands here.
- **A module should make sense with its imports unread.** Each imported name plus its
doc line should say enough that the reader never has to open the source module. If
they do, the import's name is failing, not the reader.
- **Keep strings whole.** Never build event names, flags, or error codes with template
interpolation (`` `github.${entity}.${action}` `` makes `github.pr.merged` unsearchable).
Write the full literal even when a loop feels DRYer.
- **Error messages start with a unique literal prefix**, so a message seen in a log greps
straight back to the throw site. ``throw new Error(`Webhook signature mismatch for ${id}`)``,
never ``throw new Error(`${prefix}: mismatch`)``.
- **One searchable concept per file, and keep orchestrators thin.** The code that answers
"where is X done?" should live in a module named after X — the thing a reader would
ask about, not the mechanism inside — not inline in a coordinator,
pipeline, or service class. An orchestrator should read as a sequence of calls into
well-named modules; if a reader lands in it from a search, every line should point them
one hop from the real implementation. Burying the implementation of several concepts in
one large file makes every search for any of them land on the same wall of code.
Split until each question-sized concept has one named home, then stop: a helper
meaningful only inside one concept belongs inline, and a file per tiny function
fragments one answer across several reads. The test runs both ways: a module that
answers many unrelated questions is holding more than one concept.
- **Colocate tests** (`foo.test.ts` next to `foo.ts`) so one search finds behavior and its
specification together.
- **Mark dead ends.** `@deprecated` on the old path, with a pointer to the new one.
## Quick checklist before committing
1. Would one search for each new exported name be enough to find its implementation?
2. Would swapping two arguments of the new function fail the build?
3. Is the one thing a caller must know but the signature can't say (units, timezone,
ownership, ordering) written right at the definition?
4. Do all log/error strings exist verbatim in the source?
5. Did anything change behavior without changing its name?
6. When code moved, is it gone from where it came from?
+2
View File
@@ -5,7 +5,9 @@ Kept around but rarely used.
## User-invoked ## User-invoked
- [bro](bro/SKILL.md) — Restate the last message in plain human language. - [bro](bro/SKILL.md) — Restate the last message in plain human language.
- [show-me](show-me/SKILL.md) — Help explain topics visually with concise diagrams and artifacts.
- [tmux-launch-agent](tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window. - [tmux-launch-agent](tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window.
- [visual-verification](visual-verification/SKILL.md) — Verify running desktop UI changes with screenshots and recordings.
## Model-invoked ## Model-invoked
+128
View File
@@ -0,0 +1,128 @@
---
name: show-me
description: Help the user understand the current topic visually with concise diagrams, code-shape sketches, and focused HTML artifacts.
disable-model-invocation: true
---
Help the user understand the current topic of conversation visually. Skip the preamble and keep prose brief. Pick the smallest view that makes the key point clear.
- Show logic or an algorithm as pseudocode:
```text
on(save)
if content is unchanged
return cached result
write new content
return fresh result
```
- Show runtime control flow as a call tree:
```text
submitForm
createSession
persistPrompt
launchAgent
navigateToSession
```
- Show UI structure as a component tree, including state and module boundaries that matter:
```tsx
<SessionPage> (apps/example/src/routes/session.tsx)
useSessionEvents()
<SessionToolbar>
<RunSkillButton> (packages/ui)
```
- Show file responsibility or a broad refactor as a shallow file tree:
```text
src/
├── commands/ # parses user actions
├── sessions/ # owns session state
└── transport/ # sends API requests
```
- Show component interaction, control flow, or data flow with Mermaid:
```mermaid
sequenceDiagram
participant User
participant UI
participant Daemon
User->>UI: choose command
UI->>Daemon: send expanded prompt
Daemon-->>UI: stream result
```
- Use `diff` when the point is what changes and the surrounding shape already exists. Match the diff shape to the topic.
For a component change:
```diff
<SessionPage>
useSessionEvents()
<SessionToolbar>
+ <RunSkillButton />
<SessionTimeline>
+ <SkillResultCard />
```
For a file-layout change:
```diff
src/
├── commands/
+│ └── show-me.ts # expands the slash command
├── sessions/
-└── transport.ts
+└── transport/
+ ├── client.ts
+ └── stream.ts
```
For a call-tree or call-stack change:
```diff
submitForm
createSession
persistPrompt
+ expandSkillMention
launchAgent
- navigateToSession
+ navigateToSession
+ subscribeToEvents
```
For a state or control-flow change:
```diff
on(save)
- write content
+ if content is unchanged
+ return cached result
+ write new content
+ invalidate cache
```
- Show the whole block when most of it is new, when omitted context would hide ownership or order, or when the user needs a copyable target shape:
```ts
function expandSkill(command: string): string {
const skillName = command.slice(1)
return `use the ${skillName} skill`
}
```
- For a visual UI, layout, state comparison, or concept too dense for Mermaid, write one focused HTML file — a diagram, an infographic, or a short slide deck, whichever fits the point. Match the product's colors, type, spacing, and components; use real labels and data; support desktop and mobile. Then open it for the user:
```
Bash(open path/to/show-me-{description}.html)
```
### guidance
Place each visual next to the short text it supports. Keep only the calls, files, props, states, and boundaries needed to answer the user's current question or the options to resolve the current discussion point.
You may use one of these, you may use several, it is unlikely you will use all of them. Use your judgement and don't overwhelm the user.
+41
View File
@@ -0,0 +1,41 @@
---
name: visual-verification
description: Verify running desktop UI changes with screenshots and recordings. Use when changing shell styling, layout, panels, menus, notifications, animations, transitions, or capture flows; inspect the artifacts before reporting completion.
disable-model-invocation: true
---
# Visual Verification
Use this before finishing a change with a visual effect. Automated tests do not
replace verification in the running UI.
1. Check the command needed for the selected branch with `command -v`. Start the
changed UI and keep its PID if it runs in the background.
2. Capture a screenshot for layout, styling, state, or focus changes:
```bash
screenshot_filename="${screenshot_filename:-visual-verification-candidate.png}"
hyprshot -m output -m active -f "$screenshot_filename"
```
`-m output -m active` captures the active output; `-f` sets the filename.
Use `omarchy screenshot` when interactive region selection is needed.
Capture reference and candidate states as separate files for layout or
layer-shell changes.
3. Record a short video for animation, transition, timing, capture, or
screen-recording changes:
```bash
video_filename="${video_filename:-visual-verification.mp4}"
screen-recorder -o "$video_filename"
# Exercise the changed behavior.
screen-recorder stop
```
4. Inspect each saved artifact for clipping, overlap, spacing, stale state,
focus, and visual regressions. For interactive changes, use `wtype` when
available (for example, `wtype -k Right -k Return`) and verify the resulting
state or command output.
5. Stop only the UI process started for verification, and confirm it is the
tracked PID. Finish when the changed behavior is visibly correct and every
relevant screenshot or recording has been inspected.