Base directory for this skill: {skill_dir}
Use this path to access skill files:
- References:
{skill_dir}/references/ - Workflows:
{skill_dir}/workflows/
IMPORTANT: Do NOT search the project directory for skill files.
- Use dependency injection, NEVER mocking frameworks
- Test behavior (what the code does), not implementation (how it does it)
- Run all verification tools before declaring completion
- Type safety first:
strict: true, noanywithout justification
Constants
All literal values (strings, numbers) must be module-level constants:
// ❌ REJECTED: Magic values inline
function validateScore(score: number): boolean {
return score >= 0 && score <= 100;
}
// ✅ REQUIRED: Named constants
const MIN_SCORE = 0;
const MAX_SCORE = 100;
function validateScore(score: number): boolean {
return score >= MIN_SCORE && score <= MAX_SCORE;
}
Share constants between code and tests — tests import from the module under test:
// src/scoring.ts
export const MIN_SCORE = 0;
export const MAX_SCORE = 100;
// spx/.../tests/scoring.unit.test.ts
import { MIN_SCORE, validateScore } from "@/scoring";
it("rejects below minimum", () => {
expect(validateScore(MIN_SCORE - 1)).toBe(false);
});
Dependency Injection
External dependencies must be injected, not imported directly:
// ❌ REJECTED: Direct import
import { execa } from "execa";
async function syncFiles(src: string, dest: string): Promise<boolean> {
const result = await execa("rsync", [src, dest]);
return result.exitCode === 0;
}
// ✅ REQUIRED: Dependency injection
interface SyncDeps {
execa: typeof execa;
}
async function syncFiles(
src: string,
dest: string,
deps: SyncDeps,
): Promise<boolean> {
const result = await deps.execa("rsync", [src, dest]);
return result.exitCode === 0;
}
| Priority | Source | What It Provides |
|---|---|---|
| 1 | docs/, README.md |
Project architecture, design decisions, intended APIs |
| 2 | CLAUDE.md |
Project-specific rules for Claude |
| 3 | ADRs/PDRs, specs | Documented decisions and requirements |
| 4 | This skill (SKILL.md) |
Generic TypeScript best practices |
| 5 | Existing code (reference) | Evidence of implementation, NOT authority |
CRITICAL: Existing code is NOT authoritative.
- Documentation describes intent — what SHOULD be done
- Existing code shows implementation — what WAS done (may be legacy, wrong, or outdated)
- When docs and code conflict, docs win
- When no docs exist, ASK before copying existing patterns
Never copy patterns from existing code without verifying they match documented intent.
Phase 0: Discovery (MANDATORY)
Run these searches before implementation:
# 1. Read project documentation
Read: README.md, docs/, CLAUDE.md, CONTRIBUTING.md
# 2. Check available dependencies (don't add what exists)
Read: package.json → dependencies, devDependencies
# 3. Find prior art for what you're building
Grep: function names, class names, patterns similar to your task
Glob: files in similar directories (src/utils/, src/services/, etc.)
# 4. Detect project conventions
Read: existing files in the same directory you'll write to
What to Discover
| Question | How to Find |
|---|---|
| What libraries are available? | package.json → dependencies |
| How does this project handle X? | Grep for similar patterns |
| What utilities already exist? | Glob for **/utils/**, **/helpers/**, **/fixtures/**, **/harnesses/** |
| What's the naming convention? | Read 3-5 files in the target directory |
| What error classes exist? | Grep for extends Error |
| What logging pattern is used? | Grep for logger, console.log, debug |
| How are configs structured? | Glob for **/*.config.*, **/config/** |
Discovery Anti-Patterns
// ❌ WRONG: Adding lodash when ramda is already used
import _ from "lodash"; // package.json has ramda, not lodash
// ❌ WRONG: Creating new logger when one exists
const logger = console; // Project has @lib/logger
// ❌ WRONG: Inventing naming convention
function fetch_user_by_id() {} // Project uses camelCase
// ❌ WRONG: New error class when domain errors exist
class MyError extends Error {} // Project has @/errors
Discovery Checklist
Before writing code, confirm:
- Read
package.json— know what libraries are available - Searched for prior art — found (or confirmed none exists)
- Identified naming conventions from existing files
- Found existing utilities to reuse (or confirmed none exist)
- Checked for existing error classes, loggers, configs
If discovery reveals existing patterns that conflict with this skill's guidance, follow the project's documented patterns.
The /testing-typescript skill provides:
- Detailed test level selection criteria
- Dependency injection patterns (NO MOCKING)
- Behavior-only testing approach
- Test organization for debuggability
- Test co-location in Outcome Engineering framework
Quick Reference - Testing Levels:
| Level | Infrastructure | When to Use |
|---|---|---|
| 1 (Unit) | Node.js + Git + temp fixtures | Pure logic, FS ops, git operations |
| 2 (Integration) | Project-specific binaries/tools | Claude Code, Hugo, Caddy, TypeScript compiler |
| 3 (E2E) | External deps (GitHub, network, Chrome) | Full workflows with external services |
NO MOCKING — Use Dependency Injection Instead:
// ❌ FORBIDDEN: Mocking
vi.mock("execa", () => ({ execa: vi.fn() }));
// ✅ REQUIRED: Dependency Injection
interface CommandDeps {
execa: typeof execa;
}
it("GIVEN valid args WHEN running THEN returns success", async () => {
const deps: CommandDeps = {
execa: vi.fn().mockResolvedValue({ exitCode: 0 }),
};
const result = await runCommand(args, deps);
expect(result.success).toBe(true); // Tests behavior
});
If working on a spec-tree work item (enabler/outcome):
- Invoke
spec-tree:contextualizingFIRST with the node path - If context loading fails: ABORT - do not proceed until all required documents exist
- If context loading succeeds: Proceed with implementation using loaded context
The spec-tree:contextualizing skill provides:
- Complete ancestor hierarchy (product → all ancestor nodes → target)
- All ADRs/PDRs at every level along the path
- Lower-index siblings (they constrain the target via dependency encoding)
- Target node spec with typed assertions
Example invocation:
# By node path
spec-tree:contextualizing spx/32-cli.enabler/54-commands.outcome
If spec-tree:contextualizing returns an error: The error message will specify which document is missing and how to create it. Create the missing document before proceeding with implementation.
If NOT working on spec-tree work item: Proceed directly to implementation mode with provided spec.
| Input | Mode | Workflow |
|---|---|---|
| Spec (ADR/PDR, node spec) | Implementation | workflows/implementation.md |
| Rejection feedback from reviewer | Remediation | workflows/remediation.md |
Determine your mode from the input, then follow the appropriate workflow.
Spec Is Law: The specification is your contract. Implement exactly what it says.
Test-Driven Development: Write tests first or alongside code. Tests prove correctness.
Type Safety First: Use strict TypeScript with
strict: true. Noanywithout justification.Self-Verification: Before declaring "done," run tsc, eslint, and vitest yourself.
Humility: Your code must pass review. Write code that will survive adversarial review.
Clean Architecture: Dependency injection, single responsibility, no circular imports, no deep relative imports.
| File | Purpose |
|---|---|
references/outcome-engineering-patterns.md |
Subprocess, resource cleanup, config |
references/test-patterns.md |
Debuggability-first test organization |
references/verification-checklist.md |
Pre-submission verification |
| Workflow | Purpose |
|---|---|
workflows/implementation.md |
TDD phases, code standards |
workflows/remediation.md |
Fix issues from review feedback |
Never Skip Tests: Write tests first. No exceptions.
Never Ignore Type Errors:
// WRONG
const result = someFunction(); // @ts-ignore
// RIGHT
const result: ExpectedType = someFunction();
Never Hardcode Secrets:
// WRONG
const API_KEY = "sk-1234567890abcdef";
// RIGHT
const API_KEY = process.env.API_KEY;
if (!API_KEY) throw new Error("API_KEY required");
Never Use Deep Relative Imports:
Before writing any import, ask: "Is this a module-internal file (same module, moves together) or infrastructure (lib/, tests/helpers/, shared/)?"
// WRONG: Deep relatives to stable locations — will REJECT in review
import { helper } from "../../../../../../tests/helpers/tree-builder";
import { Logger } from "../../../../lib/logging";
import { Config } from "../../../shared/config";
// RIGHT: Configure path aliases in tsconfig.json
import { Logger } from "@lib/logging";
import { Config } from "@shared/config";
import { helper } from "@testing/helpers/tree-builder";
Depth Rules:
./sibling— ✅ OK (same directory, module-internal)../parent— ⚠️ Review (is it truly module-internal?)../../or deeper — ❌ REJECT (use path alias)
Configure tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@testing/*": ["tests/*"],
"@lib/*": ["lib/*"]
}
}
}
# Type checking
npx tsc --noEmit
# Linting
npx eslint src/ test/
npx eslint src/ test/ --fix
# Testing
npx vitest run --coverage
- Spec fully implemented
- All functions have type annotations
- All public functions have JSDoc
- Tests exist for all public functions
- tsc passes with zero errors
- eslint passes with zero errors
- All tests pass
- Coverage ≥80% for new code
- No TODOs/FIXMEs unaddressed
- No console.log statements
- No hardcoded secrets
Your code will face an adversarial reviewer with zero tolerance. Write code that will survive that scrutiny.