Captain's Chest

School · Skills

Project Code Standards

Applies stack-aware project coding conventions.

When to use it

Use for implementation and refactoring in any codebase; map affected work areas, apply general standards everywhere, load TypeScript conventions only for TypeScript work, and load Angular conventions only for Angular component structure, component cuts in templates, selectors and host shape, artifact naming, forms, async data loading, or icon changes.

Principles it encodes

  • Map every affected work area and its language and framework before writing code.
  • General standards apply everywhere; TypeScript and Angular standards apply only to matching work.
  • Prefer pure functions for domain logic and keep side effects at system boundaries.
  • Prefer declarative, data-driven control flow when it is at least as clear.
  • Name numeric literals that carry domain meaning, including units.
  • TypeScript: pure logic in a domain functions.ts; interfaces in models.ts, type aliases in types.ts, constants in constants.ts.
  • TypeScript: # private members, _ for protected, readonly by default.
  • Angular: cut a child component at each clear UI region, and let the host be the element of intent.
  • Angular: one folder per component with separate files, and no framework-role suffixes in names.
  • Angular: Signal Forms and httpResource/resource by default where available.
  • Angular: icons from @captains-chest/material-symbols-rounded-icons (outline and filled).
  • Boy Scout cleanup, kept close to the change and committed separately.

Part of the Way: A pure core, with effects at the edges, Name things for what they mean, Standards apply only where they fit, Modern Angular, our own building blocks, Leave it cleaner, close to the change

How to use it

Install it into your agent with the skills manager:

npx skills@latest add captains-chest/skills --skill project-code-standards -y

The full text

SKILL.md

Project Code Standards

Apply standards through an applicability map so conventions stay within their matching work areas.

1. Map affected work areas

Before implementation, identify every project or work area the task will affect. Determine each area's language and framework from this evidence, in order:

  1. The files being changed or created.
  2. The nearest project manifests and configuration.
  3. Conventions in neighboring code.
  4. Repository-wide configuration as a fallback.

Treat mixed repositories as multiple work areas. Update the map if implementation expands into additional files or projects.

This step is complete when every anticipated file belongs to a work area with an identified language and, when relevant, framework.

2. Load matching standards

  • Always read GENERAL.md and apply it to every work area.
  • For TypeScript work areas, read TYPESCRIPT.md. Apply it only to those work areas.
  • For Angular work that creates components, decides component cuts in templates, chooses component selectors or host shape, names framework artifacts, implements forms or async data loading, or modifies icons, read ANGULAR.md. Apply it only to that Angular work area.

A branch applies only when the affected work area matches its condition. The presence of a technology elsewhere in the repository does not activate that branch.

This step is complete when every work area has the general branch and all matching technology branches, with no unmatched branch selected.

3. Implement and verify

Implement using the selected standards. Keep each branch scoped to its matching files, including during nearby cleanup.

Before finishing, account for every modified file in the applicability map and verify that all standards selected for its work area were applied. Resolve any file that does not fit the map before completion.

SKILL.md on GitHub

README.md

project-code-standards

Stack-aware project conventions for implementation and refactoring. The skill maps affected work areas before selecting standards, preventing conventions from one language or framework from leaking into another.

Branches

  • GENERAL.md — conventions shared across languages
  • TYPESCRIPT.md — TypeScript-specific conventions
  • ANGULAR.md — Angular component cuts, host/selector shape, structure, naming, Signal Forms, reactive resources, and icons

SKILL.md is the authoritative selection process. Each branch file is the authoritative source for its own conventions.

Install

npx skills@latest add captains-chest/skills \
  --skill project-code-standards -y

Extending the skill

Add a technology branch only when the project has deliberate conventions for that technology. Give it a precise applicability condition in SKILL.md; do not use a technology's presence elsewhere in a repository as sufficient activation.

README.md on GitHub

ANGULAR.md

Angular Standards

Read the matching sections for Angular work that creates components, decides component cuts in templates, chooses component selectors or host shape, names framework artifacts, implements forms or async data loading, or adds, replaces, or modifies icons.

Component cuts

When authoring or expanding a template, cut a child component at each clear UI region instead of nesting more markup in the parent. A cut is warranted when any of these hold:

  1. Collection body — inside @for, the item markup (often nested under <li>, <tr>, or a card/tile wrapper) is more than a leaf.
  2. Repeated structure — the same multi-element shape appears in more than one place in the work.
  3. Nameable UI concept — the block has a natural noun (row, card, chip, toolbar, empty state, address fields).
  4. Local interaction — the block owns its own controls or local UI state (expand, select, edit, menu).
  5. Substantial branch — an @if / @switch arm is a full UI region, not a one-line spinner or empty message.

A leaf stays inline: a single text node, a single icon or badge, or one plain control with no surrounding structure of its own.

After each cut, apply Host over wrapper and New component structure. Collection items that are list or table rows often use a native host (li[app-…], tr[app-…]):

@for (product of products(); track product.id) {
  <li app-product-row [product]="product" />
}

The parent keeps the list shell and iteration; the cut owns the item.

The decision is complete when every non-leaf @for body, repeated block, nameable region, and substantial branch in the changed templates is either cut into a component or left as an explicit leaf.

Host over wrapper

For each new component (and each component whose outer markup is in scope), choose the selector so the host is the element of intent. The template renders inside the host — do not open the template with a second copy of the landmark or control the host already is.

Prefer a native host (tag[prefix-name]) when the component is a standard element consumers should keep treating as that element (semantics, built-in APIs, ARIA on the real tag):

@Component({
  selector: 'article[app-product-card]',
  templateUrl: './product-card.html',
  styleUrl: './product-card.scss',
})
export class ProductCard {}

Consumer:

<article app-product-card [product]="p">…</article>

product-card.html holds inner structure only — not another <article>. Same pattern for button[app-…], section[app-…], nav[app-…], form[app-…], and other natives when that tag is the component.

Prefer a custom element type selector (app-user-chip) when there is no native stand-in and the component is a new UI concept. Use the project’s short prefix; never use ng.

Use an attribute-only selector ([app-dropzone]) when any of several tags may host the behavior; narrow with :not(…) when some tags must never match.

Attribute and combined selectors use lowercase dash-case attributes and the same prefix discipline as custom elements. Angular does not error on unknown attributes the way it does on unknown custom tags — import the component at every use site.

Selector decision is complete when the host carries the outer semantics or control surface, the template does not re-wrap that same tag, and the choice is custom type, native+attribute, or justified attribute-only. For supported selector kinds and matching rules, use the angular-component-selectors skill or Component selectors.

New component structure

Create each new component in a folder named for the component. Keep its TypeScript, template, and styles in separate files with the same base name:

user-card/
├── user-card.ts
├── user-card.html
└── user-card.scss

Use the project's configured stylesheet extension. Reference the external files from the component metadata:

@Component({
  templateUrl: './user-card.html',
  styleUrl: './user-card.scss',
})
export class UserCard {}

Apply this structure to new components. Preserve existing inline components unless migration or structural refactoring is explicitly in scope. Pair this file layout with Host over wrapper when setting selector.

Intent-based artifact naming

In new projects and projects already using suffixless names, omit framework-role suffixes from file and class names. Prefer user-card.ts and UserCard over user-card.component.ts and UserCardComponent.

Name injectables for their responsibility rather than the generic mechanism. For example:

session-store/
└── session-store.ts
@Injectable({ providedIn: 'root' })
export class SessionStore {}

The responsibility name replaces generic .service and Service suffixes when the surrounding folder makes the concept unambiguous. Add a role suffix only when it disambiguates multiple concepts, framework tooling requires it, or the established project convention requires it.

When an existing project consistently uses role suffixes, preserve that convention unless a naming migration is explicitly in scope.

Signal Forms by default

For each new form, inspect the project's Angular version and forms availability before choosing an API. In Angular 21 and newer projects where @angular/forms/signals is available, use Signal Forms as the default.

Choose another forms API only when a concrete constraint requires it, such as incompatible Angular versions, third-party integration, or an explicit project requirement. In projects without Signal Forms, follow the established forms strategy.

Preserve the API of an existing Reactive Forms or template-driven form unless migration is explicitly requested or already in scope.

The forms decision is complete when Signal Forms is selected or a concrete compatibility or project constraint identifies the appropriate alternative. Use the angular-developer skill for API-level Signal Forms guidance.

Reactive resources by default

For each new reactive data-loading flow, inspect the project's Angular version and API availability. Use Angular resources as the default when available:

  • Use httpResource for reactive HTTP reads that should use Angular's HTTP stack, including its interceptors.
  • Use resource for reactive reads that need a custom loader or use a non-HTTP source.

Use command-oriented APIs for mutations such as create, update, and delete operations. Use RxJS when the requirement depends on multi-emission streams, WebSockets, or complex event-stream composition. Compatibility, third-party integration, and explicit project requirements may also determine another approach.

Preserve existing Observable, HttpClient, and promise-based loading flows unless migration is explicitly requested or already in scope.

The data-loading decision is complete when httpResource or resource is selected, or a concrete requirement identifies the appropriate alternative. Use the angular-developer skill's resource guidance for API-level implementation details.

Icons

Use icons from these Angular packages:

  • @captains-chest/material-symbols-rounded-icons
  • @captains-chest/material-symbols-rounded-icons-filled
import { HomeIcon } from '@captains-chest/material-symbols-rounded-icons';
import { HomeFilledIcon } from '@captains-chest/material-symbols-rounded-icons-filled';

Prefer an icon from these packages over custom artwork or another icon set. When neither package can satisfy the requirement, pause and ask for explicit approval before introducing custom artwork or a different package.

ANGULAR.md on GitHub

GENERAL.md

General Standards

Apply these conventions to every affected work area in a way that is idiomatic for its language and consistent with the project's established architecture.

Functional-first design

  • Prefer pure functions for domain logic when practical.
  • Place pure logic in the nearest idiomatic, domain-local module. Follow the language and project's existing structure rather than introducing a technology-specific file pattern.
  • Keep side effects at system boundaries such as framework integration, API calls, persistence, I/O, and UI wiring.
  • Keep domain logic out of components and service classes when a pure domain abstraction expresses it more clearly.

For example, separate a price calculation from the operation that loads or saves an order. The calculation belongs in a pure domain abstraction; persistence remains at the boundary.

Intention-revealing control flow

  • Prefer declarative composition when it is idiomatic and at least as clear as imperative flow.
  • Prefer data-driven mappings over long conditional chains when behavior is selected by data.
  • Use imperative flow when it improves clarity, performance, resource handling, or framework integration.
  • Explain only surprising or non-obvious trade-offs; ordinary idiomatic control flow needs no justification comment.

Meaningful numeric literals

Name a numeric literal when the name adds domain meaning, units, policy context, or protocol meaning. Typical candidates include:

  • Timeouts and durations
  • Limits and thresholds
  • Retry policies
  • Rates and percentages
  • Protocol or status values

Include units in names when relevant, such as sessionTimeoutMs or MaxRetryCount.

Keep a literal inline when its meaning is locally obvious and extraction would reduce readability. Apply the same test in production code and tests.

Boy Scout cleanup

  • Leave the directly affected area cleaner when a nearby improvement is safe and relevant.
  • Keep cleanup proximal to files and domains already involved in the task.
  • Track and summarize cleanup separately from the primary change.
  • When the user requests commits, put cleanup in a separate commit using the dedicated commit skill.
  • When commits are not requested, leave the changes uncommitted.

GENERAL.md on GitHub

TYPESCRIPT.md

TypeScript Standards

Apply these conventions only to TypeScript work areas.

Pure-function placement

Move domain logic that can be pure into the nearest appropriate domain functions.ts file. Create that file when the domain has no suitable one. Promote logic to a shared domain functions.ts only when it is genuinely shared; avoid unrelated global utility modules.

// cart/functions.ts
export const calculateCartTotal = (items: CartItem[]): number =>
  items.reduce((total, item) => total + item.price * item.quantity, 0);

Framework hooks, API calls, persistence, and UI wiring should call these functions from their boundaries.

Domain declarations

Separate exported domain declarations by kind in the nearest owning domain:

  • Interfaces belong in models.ts.
  • Type aliases belong in types.ts.
  • Shared constants belong in constants.ts.

Export declarations from those files when they form part of the domain or cross a file boundary. Keep implementation-private interfaces, types, and constants colocated with their owner and unexported. Do not widen a module's public surface solely to satisfy file placement.

Prefer domain-local declaration files over repository-wide catch-all files. Promote a declaration to a shared domain only when it is genuinely reused there.

Class members

  • Use # private fields and methods.
  • Prefix protected fields and methods with _.
  • Mark properties readonly when they are not reassigned after initialization.
  • Prefer constructor-initialized readonly fields; introduce mutability only when behavior requires it.
export class UserCache {
  readonly config: Config;
  protected readonly _retryLimit: number;
  #expiresAt: Date;

  constructor(config: Config, retryLimit: number, expiresAt: Date) {
    this.config = config;
    this._retryLimit = retryLimit;
    this.#expiresAt = expiresAt;
  }

  #isExpired(now: Date): boolean {
    return now >= this.#expiresAt;
  }
}
Runtime-access exceptions

Use TypeScript private only when a framework or tool requires a string-addressable runtime property, such as:

  • Reflection, serialization, or ORM metadata
  • Instrumentation that must patch or spy on internals
  • Interop boundaries that access members by name

Choose the narrowest viable visibility, retain readonly where possible, and explain the runtime constraint with a short comment.

TYPESCRIPT.md on GitHub

Built from captains-chest/skills at fcb40fb. Credits: parts of the skills repository's agent setup (the agent docs and triage-label vocabulary) and some of its conventions are adapted from Matt Pocock's skills.