Patterns, rules, and lessons learned while building ytstack. This file is read by every future session. Keep it short. Keep it actionable.
- UX contracts are mandatory. Every skill must pass
docs/ux/*checks.bin/ytstack-checkenforces them locally (run before commit); there is no UX-contract CI gate, onlysecret-scan.yml. Non-procedural skills opt out of the structural sections viakind: directiveorkind: reference(see DECISIONS 2026-05-31 + skill-structure.md). - No 1:1 copying from upstream. Vendored superpowers/gstack content is read-only. If we need to change behavior, wrap it in a ytstack-namespaced skill, don't modify the vendor.
- Artifact-first thinking. If a decision or learning would help future sessions, write it to
DECISIONS.mdorKNOWLEDGE.md, not just conversation. - Third-party methodology attribution is "inspired by", never verbatim. Concepts adapted, prose rewritten. Do not name the external source in public artifacts.
- Hook matcher
startup|clear|compactis the key. SessionStart hook only fires on these three events./reload-pluginsdoes NOT fire it, which means mid-session plugin enable produces dead-looking skills. Always test with/clearafter install. - The
using-<plugin>skill is a hook-injected prompt, not a regular skill. superpowers demonstrates this: the SessionStart hook readsskills/using-superpowers/SKILL.mdand injects the full content viaadditionalContext. That's why auto-triggering works -- it's forceful prompt priming, not passive skill availability. - Agent Teams replicate GSD's fresh-context-per-task natively. Each teammate is a separate Claude Code session with its own 200k window. No need to build this ourselves.
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1required to enable Agent Teams. Version v2.1.32+ required.
-
Competitive claims require skill-inventory checks. Initial README drafts (2026-04-24) overclaimed that superpowers "offers nothing for planning" and gstack "offers nothing for execution." Both wrong. superpowers has
brainstorming,writing-plans,executing-plans. gstack hasinvestigate(equivalent to superpowers' systematic-debugging),qa,review,ship, and context-save/restore skills. The actual ytstack value-prop is cherry-picking the non-overlapping best from each, plus adding GSD-style artifact hierarchy that neither has. Lesson: before any "X tool offers nothing for Y" statement, grep the actual skill dirs. -
Project-state injection alone is not enough -- you need a directive. The initial M002 SessionStart hook injected only project state (milestone / slice / task + next action). That told the agent where it was, but not what to do with ytstack. Result: ytstack skills sat dormant unless the user typed
/ytstack:<name>manually. The superpowers pattern is clearer: their SessionStart hook injects the entire content ofusing-superpowers/SKILL.md-- a forceful directive with anti-rationalization Red Flags and "1% chance → invoke" compliance pressure, and skill selection driven by each skill's owndescription:field (semantic match, no phrase table). Without this primer, the plugin is a slash-command menu the user has to drive. With it, the agent reaches for skills proactively based on natural-language intent. See DECISIONS 2026-04-24 "Add using-ytstack" + 2026-04-24 "Skill selection is semantic" for ytstack's version (we initially added a trigger-map alongside the directive; rolled back same day as antipattern). Lesson: a SessionStart hook that only injects state produces a passive plugin; it needs to also prime the agent's behavior. But do NOT reach for a phrase-map -- sharpen descriptions instead. -
Don't cargo-cult templates. Cross-reference dependent files before designing a new artifact. While writing the three
agents/files (post-M009), we usedsuperpowers/agents/code-reviewer.mdas template and carried over its frontmatter verbatim -- which didn't include askills:field. Result: three ytstack agents that are supposed to invoke ytstack skills, with noskillsallowlist. The miss was caught on review ("die agents haben keine skills?????"), not by process. Root cause: we reached for a template instead of re-readingskills/spawn-milestone-team/SKILL.md(which references these agents and specifies what skills they'll use) and designing from purpose. Lesson: when creating a new file type (skill, agent, hook), FIRST read every other file that references or depends on this new file, THEN design from purpose. Templates are reference, not contract. Template-driven development produces files that look right and miss the point. Seedocs/ux/agent-structure.md(added to prevent this exact miss in future) and the 2026-04-24 DECISIONS entry on using-ytstack. -
Bulk find-replace can corrupt meta-references. While fixing 288 em-dash (
—) warnings by bulk-replacing—→--, the same replacement hit docs/ux/writing-style.md's banned-punctuation rule itself (which was literally describing the banned—character), leaving the rule reading "Em dashes (--)" -- self-contradictory. Same with KNOWLEDGE.md's lesson and REVIEW-NOTES' entry about the same issue. Lesson: before any bulk text replacement, grep for files where the text-being-replaced is the subject of discussion (meta-references), not just content. Exclude or post-edit those files. Simple heuristic: if a file contains phrases like "banned", "converts X to Y", "rule about Y", the Y-references in that file are probably meta. -
Plugin skills (
skills/) vs project-meta skills (.claude/skills/) are distinct scopes; classify before writing. Surfaced 2026-04-24: a freshly-authoredcheck-consistencyskill (which audits ytstack's own docs-vs-code drift) was placed underskills/by default -- which would have shipped an internal-audit tool to every ytstack end user as if it were part of the plugin's value prop. Rule: anything that exists to help build / audit / maintain ytstack itself belongs under.claude/skills/. Anything that ships to ytstack users belongs underskills/. Before creating a new skill, ask: is this for end users of the plugin, or for ytstack-dev? Place accordingly. Don't "drop it under skills/ for now and move later" -- default placement shapes the mental model. -
DRAFT synthesis papers must stay clearly subordinate to their source. Surfaced 2026-04-24: a concept paper was generated in-session and immediately treated as authoritative, including landing a DECISIONS.md entry that "settled" an architectural question based on the unapproved paper. Rule: derivative docs (concept, summaries, inventories) are always subordinate to their source (README / DECISIONS). When synthesis and source disagree, source wins until the human explicitly signs off on the synthesis. Never use unapproved synthesis to drive DECISIONS entries or README edits.
-
Append-only DECISIONS means approvals, not proposals. A DECISIONS entry records a locked choice the user has accepted. Writing one based on "the concept paper implies X" (when the paper itself is unapproved) corrupts the register. Always check: has the user explicitly agreed to this? If not, don't file. Proposals go in REVIEW-NOTES, not DECISIONS.
-
Context loss is structural, not incidental. "Remember this" or "keep it in your context" doesn't work across compaction. The fix is persistent: condensed memory file + source-of-truth pointers in CLAUDE.md + explicit
read these three files before proposing changesritual. Surfaced 2026-04-24 when I repeatedly forgot the curation scope and invented architecture rules ("strict gstack-vs-superpowers split") that contradicted README. Now captured as auto-memoryproject_ytstack_readme_facts.mdwith re-read instructions. -
Don't ask obvious clarifying questions. If the user has just told you what to do and the next step is self-evident, execute; don't re-ask for permission. "Should I X?" after they said "do X" is noise, erodes trust, and signals you didn't parse the instruction. Surfaced 2026-04-24 repeatedly.
-
.cursor-plugin/plugin.jsonis a different schema than.claude-plugin/plugin.json. The superpowers repo contains BOTH. The.cursor-plugin/variant declares"skills": "./skills/","agents": "./agents/","commands": "./commands/","hooks": "./hooks/hooks-cursor.json"-- that is the Cursor format. The.claude-plugin/variant is minimal (no path pointers). Reading the Cursor file when researching Claude Code plugin structure produces 4+ iterations of wrong "fixes" that the CC install validator rejects. Rule: when researching plugin manifest fields, read.claude-plugin/plugin.jsonfrom a known-working CC plugin (e.g.claude-md-management), never.cursor-plugin/plugin.json. Surfaced 2026-04-24 during the install debacle. -
Claude Code plugin components are implicitly auto-discovered. Adding explicit path pointers in plugin.json causes duplicate-load errors or validation rejects. Per CC plugins-reference docs: "Skills and commands are automatically discovered when the plugin is installed" from
skills/in plugin root. Hooks auto-load fromhooks/hooks.json. Agents auto-load fromagents/. Adding"skills": "./skills/"or"hooks": "./hooks/hooks.json"to plugin.json triggers "Duplicate hooks file detected" or "agents: Invalid input" on install. Rule: plugin.json stays minimal -- name, version, description, author, homepage, repository, license, keywords. No component path pointers. Let auto-discovery handle it. -
/reload-pluginsoutput counts are undocumented and misleading; use/skills,/plugin,/agentsfor authoritative audits. The/reload-pluginsline "Reloaded: N plugins · M skills · X agents · Y hooks · ..." has no documented semantics beyond "reports counts for each reloaded component". Counts do not match simple total-across-plugins or delta-since-last-reload interpretations. A plugin shipping 16 skills may report "1 skill" on reload. Rule: to verify what's actually registered, open/skills(lists all registered skills with scope + token cost) or/plugin(Installed tab shows per-plugin components). Do not debug install issues from/reload-pluginscounts. Surfaced 2026-04-24 after 9 commits chasing a nonexistent bug. -
Excalidraw rendering needs a version-pinned esm.sh import.
https://esm.sh/@excalidraw/excalidraw?bundle(no version) hits a transitive 404 on@braintree/sanitize-urland the renderer hangs. Pin to@excalidraw/excalidraw@0.18.0?bundle. Pattern:cp render_template.html render_template.html.bak && sed -i '' 's|...|...@0.18.0?bundle|' render_template.html && uv run python render_excalidraw.py --output ... && mv render_template.html.bak render_template.html. Always restore the template after the render to avoid leaking a per-project pin into the global skill. Surfaced 2026-04-24/25 across all three workflow infographics. -
Excalidraw dark mode is a render flag, not a color swap. First attempt at a dark variant of the greenfield infographic copied the
.excalidrawsource and replaced fill/stroke colors -- looked muddy and unbalanced because Excalidraw's dark theme also flips contrast and shadows. The correct path: keep ONE.excalidrawsource of truth, render twice -- once withappState.exportWithDarkMode = false(light), once with= true(dark). Patch the samerender_template.htmllike the version pin. User caught the color-swap as fake on first review. Source-of-truth-stays-singular even for visual variants. -
GitHub Flavored Markdown
<details open>is the lever for default-expanded sections.<details><summary>...</summary>...</details>collapses by default. Addopento the opening tag (<details open>) to render expanded but still collapsible. Use for "primary" diagrams the reader should see immediately, while keeping secondary ones collapsed. Pair the summary text with the actual state ("click to expand" when closed-by-default, "click to collapse" whenopen). Used 2026-04-25 in README Workflows section. -
Plugin-marketplace
owneris a required object..claude-plugin/marketplace.jsonREQUIRES a top-level"owner": { "name": "...", "url"|"email": "..." }object. Missingownercauses install to fail withowner: Invalid input: expected object, received undefined. Similarlyplugins[].authoris expected (seen in superpowers, not strictly required but recommended). Self-marketplace"source": use"./"to match superpowers, not"./."(both may work but./is the convention). Surfaced 2026-04-24 during first install attempt.
-
Each bash code block in a SKILL.md runs in a separate shell. Variables don't persist between blocks. Pass state via prose ("remember as
_X") or disk files, never via shell variables across blocks. -
Dot-prefixed directories under
~/.claude/skills/are hidden from skill discovery. We exploit this for.deactivated/snapshots. Claude Code only scans immediate non-dot children. -
gstack SKILL.md files are auto-generated from
.tmplviabun run gen:skill-docs. When reading vendored gstack content for reference, compare with the template, not just the generated MD. -
Only the em-dash rule is mechanically enforced; the old banned-words list was retracted. Per DECISIONS 2026-04-24 "drop banned-vocabulary list (scope creep)",
bin/ytstack-checkflags only em-dashes (the U+2014 character; use--). macOS autocorrect silently converts--to that character, so disable smart-punctuation or runbin/ytstack-checkpre-commit. The rest ofdocs/ux/writing-style.md(short sentences, concrete nouns, no delve/robust/comprehensive) is quality guidance reviewers apply, not a grep gate. -
Headless
claude -pblocks on Write permissions by default. In M001 T05 smoke-test, first headless run stalled because-pmode cannot display or approve the Write-tool permission dialog. Fix: pass--permission-mode acceptEditsfor file-creating skills, or--dangerously-skip-permissionsfor fully isolated sandbox/CI runs. Document this prominently for any automated-test scenario. -
Headless mode may not register
--plugin-dirskills forSkilltool invocation. In the same smoke-test, the headless Claude reported that theSkilltool invocation forytstack:init-projecterrored as "not registered in the harness", but the agent fell back to following the SKILL.md procedure manually -- producing correct output. This means: (1) our skills must work as "prompted instructions" without requiring theSkilltool, and (2) real interactive testing is still needed to verify theSkilltool registration works in non-headless mode. Worth an upstream bug report once reproduced in interactive mode. -
RemotePluginSync(Claude Desktop + SSH) refuses ANY symlink in a plugin directory and aborts the whole sync. Reported failure:RemotePluginSync: refusing symlink in plugin directory: vendor/gstack/bin/gstack-brain-reader. Sources of symlinks in this repo: vendored upstream symlinks (e.g.vendor/gstack/connect-chrome,vendor/gstack/bin/gstack-brain-reader,vendor/superpowers/AGENTS.md -> CLAUDE.md) AND, since 2026-05-31, our own repo-rootCLAUDE.md -> AGENTS.mdshim. Native install on the remote host is unaffected; only Desktop+SSH-sync trips it. UNVERIFIED for the new root symlink -- verification path: install via a Desktop SSH session against a remote host and watch for therefusing symlinkerror. Mitigation sketch (from an old, superseded draft in theclaude/thirsty-bhabha-a03490worktree): dereference symlinks insync-upstream.shafter each subtree pull (find vendor -type l | ... rm + cp -R), idempotent. Not yet planned as a milestone. -
A literal triple-backtick inside a
```!executable block crashes the preamble for every user. (M012 / issue #20) The harness extracts the executable block by scanning for the next fence; a literal triple-backtick in ased/awkpattern (e.g.sed -n '/^```/,...') terminates the extraction early, so the captured bash is truncated and fails withunmatched '/parse error. Environment-independent. Fix: generate the fence at runtime with_FENCE=$(printf '\140\140\140')and match fences by PREFIX (index($0,f)==1, handles language-tagged opening fences like```bash); never write a literal triple-backtick inside the block. This crashedverification-before-completionAND, on spawn, theytstack:implementer/ytstack:verifiersubagent types -- they run a skill preamble at startup, so a broken preamble takes the agent down before it does any work. -
Hooks receive
session_idon stdin JSON, not from an env var. (M012 / issue #21-RC2) There is noCLAUDE_SESSION_IDenv var; reading it always yields the fallback, sosession-endrecordedsession_id:"unknown"on every entry. Fix: capture stdin once near the top (INPUT=$(cat)) and parseprintf '%s' "$INPUT" | jq -r '.session_id // "unknown"'. Available hook env vars areCLAUDE_PROJECT_DIR,CLAUDE_PLUGIN_ROOT,CLAUDE_PLUGIN_DATA,CLAUDE_ENV_FILE,CLAUDE_EFFORT,CLAUDE_CODE_REMOTE-- none carry the session id. -
Subagents load skills/hooks from the installed plugin CACHE, not your working tree. A fix committed to the repo is INERT in the running session until a version bump + release +
/plugin update. Corollary: a bug in a published skill (e.g. #20) keeps reproducing in-session and crashes ytstack subagent types even after you have committed the fix. Dev workaround when a ytstack skill is broken: dispatchgeneral-purposeagents with self-contained prompts (they do not auto-run the broken ytstack preamble) instead of theytstack:*agent types. -
slice-milestonedetects unplanned slices only by the literal string "(to be planned)". (M012) Ifplan-milestonepre-fills the slice goals in the roadmap, the detector finds zero unplanned slices and short-circuits even though noM###-S##-PLAN.mdfiles exist yet. More robust signal: presence of the per-slice PLAN files, not the placeholder text. -
Uncommitted milestone planning in a stale worktree causes number collisions. (M012) A prior session left an uncommitted "M012 = vendor symlink dereferencing" plan in the
claude/thirsty-bhabha-a03490worktree; because it never reachedmain, a later session reused M012 for the swarm fix-pack. Lesson: reserve milestone numbers in the committedROADMAP.mdtable, never only in an uncommitted worktree. The vendor-symlink work is reserved as M014. -
A PostToolUse hook that writes a TRACKED file after every commit can never settle the tree. (M015 / issue #22) The describing-line is written after the commit it describes, so it is always uncommitted; committing it re-fires the hook; an always-dirty
.ytstackdraft is invisible to a worktree branched frommain, which silently breaksspawn-milestone-teamdispatch. Two compounding bugs made it worse: a glob matcher*git*commit*that fired on echoes /git log | grep commit/ doc strings, and agit rev-parse HEADappend with no new-commit check and no dedup (N identical SHAs, N timestamps). Fix pattern: the hook is stub-once (create the draft once, then[ -f "$SUMMARY_FILE" ] && exit 0-- never re-write), and the durable commit-to-task list is owned bysummarize-task, which grepsgit log --grep "^M###-S##-T##"(ytstack's commit convention) into a committed## Commitssection. Matcher tightened to requiregit commitafter a command separator ((^|[;&|()])[[:space:]]*git[[:space:]]+commit) -- a raw lexical matcher cannot perfectly distinguish a real invocation from an echo of the phrase, but stub-once makes a rare false-positive harmless (one premature stub, overwritten at close). Regression guard:tests/post-tool-use-bash.test.sh. -
Vendor-drift audit: only the six
cat-ed wrapped SKILL.md matter; check path-existence first (silent break), then content. (2026-05-31) The wrappers inline upstream via```!cat vendor/<repo>/.../SKILL.md, so the breaking surface is exactly those six files. Method: shallow-clone both upstreams, (1) confirm eachcat-ed path still exists upstream -- a rename makes thecatsilently empty and the wrapper produces nothing; (2) diff the six SKILL.md. Result of the 2026-05-31 audit: superpowers6efe32c -> 6fd4507left all three wrapped SKILL.md byte-identical (safe, pulled + on origin/main asca6675e); gstackaeea57f -> 3bef43bis a v1 restructure (~1000 changed lines/file) but the methodology anchors (forcing questions, CEO modes, eng anchors) all survive -- no hard break. The.tmpldiffs are irrelevant; the wrappercats the rendered SKILL.md, not the template. -
gstack-v1 adds a
gbrain:frontmattercontext_queriesblock -- NOT covered by the "Vendored-preamble drift accepted" decision. (2026-05-31) That 2026-04-25 decision accepts gstack's shell preamble failing via|| true. gstack-v1'sgbrain:is YAML frontmatter (queries against~/.gstack/builder-profile.jsonl,eureka.jsonl, etc.), plus a newGSTACK_PLAN_MODEdetection. Frontmatter is not|| true-guarded shell, so before pulling gstack, re-open DECISIONS 2026-04-25 "Vendored-preamble drift accepted" and decide how the wrapper should treatgbrain. Also note the existing preamble already side-effects beyond silent failure:mkdir -p ~/.gstack/sessions, analytics-jsonl appends, and telemetry/feature-discoveryAskUserQuestioninstructions the agent may follow inside a ytstack session.
- Milestone (M###): a shippable chunk of the roadmap. 1-7 slices per milestone.
- Slice (S##): 1-7 tasks inside a milestone, executable sequentially.
- Task (T##): one atomic unit that fits in one context window. If it doesn't, split into two tasks.
- Artifact: a persistent file in
.ytstack/that survives across sessions. - Spec: a design doc written before code, committed under
docs/ytstack/specs/. - Handoff: state written when a session ends, read when the next session starts.
- Sentinel file: marker in
~/.ytstack/.<name>-promptedthat indicates an "ask-once" question has been answered. - Tier: question scope --
core(session-wide, persisted),task(single turn, not persisted),background(skippable, default-pickable).
- superpowers -- vendored in
vendor/superpowers/from M005. https://github.com/obra/superpowers - gstack -- vendored in
vendor/gstack/from M004. Garry Tan's framework. - GSD v2 -- NOT vendored, but informs our artifact model. https://github.com/gsd-build/gsd-2