Purpose: This guide captures the conventions, patterns, and gotchas specific to this codebase. Read this before making changes to avoid common pitfalls!
- Quick Start
- Import Rules (CRITICAL)
- File Structure & Naming
- Code Conventions
- Commit Message Format
- Testing & Building
- Common Gotchas
# 1. Clone and install
git clone <repo-url>
cd the-human-pattern-lab-cli
npm install
# 2. Build
npm run build
# 3. Test locally
npm start version
# 4. Install globally (optional)
npm install -g .
hpl version
# 5. Run tests
npm test- ✅ ES Modules: This project uses ES Modules (
"type": "module") - ✅ Import Extensions: ALL relative imports MUST include
.js(even for.tsfiles) - ✅ Lore-Coded Commits: Use emoji prefixes (see Commit Messages)
- ✅ Contract-First: Output formats are contracts - changes are breaking
- ✅ JSON Purity:
--jsonmode MUST only emit JSON to stdout
Rule: ALL relative imports MUST end in .js, even though the source files are .ts.
Node.js ES Modules require explicit file extensions. TypeScript doesn't add them automatically, so you must write them yourself.
// ❌ WRONG - Will cause ERR_MODULE_NOT_FOUND at runtime
import { something } from "./utils"
import { other } from "../lib/config"
import { helper } from "./helpers/index"
// ✅ CORRECT - Add .js to all relative imports
import { something } from "./utils.js"
import { other } from "../lib/config.js"
import { helper } from "./helpers/index.js"
// ✅ ALSO CORRECT - npm packages don't need extensions
import { Command } from "commander"
import { z } from "zod"
import fs from "node:fs"Before committing, search for potential missing extensions:
# Find imports that might be missing .js
grep -r "from ['\"]\.\.*/[^'\"]*[^s]['\"]" src/ --include="*.ts" | grep -v "\.js['\"]"Add to .vscode/settings.json:
{
"typescript.preferences.importModuleSpecifierEnding": "js",
"typescript.preferences.importModuleSpecifier": "relative"
}This makes VS Code auto-add .js when using auto-import!
src/
├── commands/ # CLI command implementations
│ ├── version.ts
│ ├── capabilities.ts
│ └── notes/ # Domain-specific commands
│ ├── notes.ts # Domain root (assembler)
│ ├── list.ts
│ ├── get.ts
│ └── create.ts
├── contract/ # Output contracts & schemas
│ ├── envelope.ts # Success/error wrappers
│ ├── intents.ts # Intent registry
│ ├── schema.ts # Zod schemas
│ └── exitCodes.ts
├── lib/ # Shared utilities
├── http/ # HTTP client
├── sdk/ # SDK exports
└── io.ts # Input/output helpers
Files: camelCase.ts or kebab-case.ts (be consistent within a directory)
Types: PascalCase
Functions: camelCase
Constants: SCREAMING_SNAKE_CASE (for true constants) or camelCase (for config)
Enums: PascalCase (enum name) and SCREAMING_SNAKE_CASE (values) or camelCase (values)
// Command files: <commandName>.ts
src/commands/version.ts
src/commands/health.ts
// Domain folders: <domain>/<domain>.ts is the assembler
src/commands/notes/notes.ts // Mounts subcommands
src/commands/notes/list.ts // Individual subcommand
// Contract files: singular nouns
src/contract/envelope.ts
src/contract/schema.tsAll source files should have a lore-coded header:
/* ===========================================================
🌌 HUMAN PATTERN LAB — <DESCRIPTION>
-----------------------------------------------------------
Purpose: <What this file does>
Contract: <Any contractual guarantees, if applicable>
Notes:
- <Important implementation detail 1>
- <Important implementation detail 2>
=========================================================== */Within a file, order from public → private, top → bottom:
// 1. Exports first (public API)
export function publicFunction() { ... }
export type PublicType = { ... }
// 2. Internal helpers below
function helperFunction() { ... }
// 3. Constants at top or bottom (be consistent)
const INTERNAL_CONSTANT = "value";Commands follow this structure:
import { Command } from "commander";
import { writeHuman, writeJson } from "../io.js";
import { EXIT } from "../contract/exitCodes.js";
import { getAlphaIntent } from "../contract/intents.js";
import { ok, err } from "../contract/envelope.js";
type GlobalOpts = { json?: boolean };
export function myCommand(): Command {
return new Command("mycommand")
.description("What this command does (contract: intent_name)")
.action((...args: any[]) => {
const cmd = args[args.length - 1] as Command;
const rootOpts = (cmd.parent?.opts?.() ?? {}) as GlobalOpts;
const result = runMyCommand();
if (rootOpts.json) {
writeJson(result);
} else {
writeHuman("Human-friendly output");
}
process.exitCode = EXIT.OK;
});
}
// Core logic separated from commander adapter
export function runMyCommand() {
const intent = getAlphaIntent("my_intent");
return ok("mycommand", intent, { data: "here" });
}// ✅ GOOD - Return error envelopes
try {
const data = await fetchData();
return ok("command", intent, data);
} catch (error: any) {
return err("command", intent, {
code: "E_NETWORK",
message: "Failed to fetch data",
details: { originalError: error.message }
});
}
// ❌ BAD - Don't throw unhandled errors
const data = await fetchData(); // Could throw!// ✅ GOOD - Use Zod for runtime validation
const DataSchema = z.object({
id: z.string(),
count: z.number()
});
type Data = z.infer<typeof DataSchema>;
// Validate at runtime
const data = DataSchema.parse(unknownData);
// ❌ BAD - Assuming types without validation
const data = unknownData as Data; // No runtime check!This repo uses emoji-prefixed commit messages following the lab's department system:
<emoji> <scope>: <subject>
<optional body>
<optional footer>
Engineering & Code (SCMS)
⚙️ feat:- New features🐛 fix:- Bug fixes🔧 refactor:- Code restructuring (no behavior change)⚡ perf:- Performance improvements🏗️ build:- Build system changes
Documentation & Knowledge (KROM)
📚 docs:- Documentation changes📝 content:- Content updates🎨 style:- Code style changes (formatting, no logic change)
Testing & Quality (QA)
✅ test:- Adding or updating tests🧪 experiment:- Experimental features
Infrastructure & Operations
🚀 deploy:- Deployment changes🔒 security:- Security improvements🌉 bridge:- Relay/bridge system changes (Liminal Bridge)
# Good commit messages
⚙️ feat: Add relay generation command
🐛 fix: Add missing .js extensions to contract imports
📚 docs: Update README with relay examples
🔧 refactor: Extract HTTP client to separate module
✅ test: Add tests for envelope builders
# Include body for complex changes
⚙️ feat: Implement notes sync command
Add bidirectional sync between local markdown files and API.
Supports dry-run mode and conflict resolution.
Closes #42- Starts with appropriate emoji
- Scope is relevant (feat/fix/docs/etc)
- Subject line ≤ 50 chars (aim for this)
- Subject is imperative mood ("Add" not "Added")
- Body explains WHY, not WHAT (code shows what)
- Breaking changes noted in footer
# Clean build
rm -rf dist/
npm run build
# Watch mode (development)
npm run dev
# Run without building (tsx)
npm run dev version# Run all tests
npm test
# Watch mode
npm test:watch
# Single test file
npm test -- src/__tests__/config.test.ts# 1. Build
npm run build
# 2. Test built version
npm start version
# 3. Install globally from local
npm install -g .
# 4. Test global install
hpl version
hpl version --json
# 5. Check actual output
hpl version --json | node -e "JSON.parse(require('fs').readFileSync(0,'utf8'))"Symptom: Error [ERR_MODULE_NOT_FOUND]: Cannot find module
Cause: Relative import missing .js extension
Fix: Add .js to the import in the SOURCE file (not compiled)
// ❌ Before
import { foo } from "./bar"
// ✅ After
import { foo } from "./bar.js"Symptom: Type checks pass but crashes at runtime
Cause: TypeScript types don't validate data at runtime
Fix: Use Zod schemas for runtime validation
// ✅ Do this
const UserSchema = z.object({
name: z.string(),
age: z.number()
});
const user = UserSchema.parse(unknownData); // Runtime check!Symptom: --json mode contains non-JSON output
Cause: Console logs or errors going to stdout
Fix: Use stderr for logs, stdout ONLY for JSON
// ❌ BAD
console.log("Fetching data..."); // Goes to stdout!
if (opts.json) writeJson(result);
// ✅ GOOD
if (!opts.json) {
console.error("Fetching data..."); // stderr only
}
writeJson(result); // stdoutSymptom: Commands succeed but return non-zero exit code
Cause: Not setting process.exitCode properly
Fix: Always set explicit exit codes
import { EXIT } from "../contract/exitCodes.js";
// Success
process.exitCode = EXIT.OK;
// Errors
process.exitCode = EXIT.ERROR;
process.exitCode = EXIT.INVALID_INPUT;Symptom: Old version runs after npm install -g .
Cause: npm cache or permission issues
Fix: Full reinstall
# Uninstall
npm uninstall -g @thehumanpatternlab/hpl
# Clear cache
npm cache clean --force
# Reinstall
cd /path/to/the-human-pattern-lab-cli
npm run build
npm install -g .Symptom: Tests fail on Windows but pass on Mac/Linux
Cause: Hardcoded / separators instead of path.join()
Fix: Use Node.js path module
import path from "node:path";
// ❌ BAD
const filePath = `${dir}/file.txt`;
// ✅ GOOD
const filePath = path.join(dir, "file.txt");The CLI's output format is a contract - a stable interface that scripts and agents depend on. Breaking the contract breaks automation.
- Schema versioning: All JSON output includes
schemaVersion - Intent disclosure: All commands declare their
intent - Envelope structure: Success/error formats are stable
- Exit codes: Deterministic exit codes for each scenario
- Additive only: In v0.x, we can ADD but not CHANGE/REMOVE
Breaking Changes (require major version bump):
- Changing envelope structure
- Removing fields from JSON output
- Changing exit code meanings
- Renaming commands or flags
- Changing intent IDs
Non-Breaking Changes (safe in minor versions):
- Adding new commands
- Adding optional fields to output
- Adding new intents
- Improving error messages
- Internal refactoring
# 1. Create feature branch
git checkout -b feat/my-feature
# 2. Make changes
# ... edit files ...
# 3. Build and test
npm run build
npm test
npm start <command>
# 4. Commit with lore-coded message
git add .
git commit -m "⚙️ feat: Add my feature"
# 5. Push and create PR
git push origin feat/my-feature- Build succeeds:
npm run build - Tests pass:
npm test - Linter happy:
npm run lint(when added) - All imports have
.jsextensions - Added tests for new features
- Updated docs if needed
- Commit message follows format
Most Common Commands:
npm run build # Compile TypeScript
npm start <cmd> # Run built version
npm run dev <cmd> # Run with tsx (no build)
npm test # Run tests
npm install -g . # Install globally from localEmergency Debugging:
# Imports not working?
grep -r "from ['\"]\.\.*/[^'\"]*[^s]['\"]" src/ --include="*.ts" | grep -v "\.js['\"]"
# Clean slate
rm -rf dist/ node_modules/
npm install
npm run build
# Global install issues
npm uninstall -g @thehumanpatternlab/hpl
npm cache clean --force
cd /path/to/repo && npm run build && npm install -g .Key Files:
src/contract/schema.ts- Output contract definitionssrc/contract/intents.ts- Intent registrysrc/io.ts- stdout/stderr helpersbin/hpl.ts- CLI entrypoint
- README.md: High-level overview and usage
- IMPLEMENTATION_NOTES.md: Architecture decisions
- docs/: API documentation and guides
- Look at existing commands for patterns
- Check contract files for schema examples
- Run
npm testto see expected behavior - Ask in #engineering channel (if applicable)
This guide should evolve with the codebase. If you:
- Find a new gotcha: Add it to Common Gotchas
- Establish a pattern: Document it in Code Conventions
- Change a rule: Update relevant sections and note breaking changes
Keep this guide:
- Practical: Focus on actionable advice
- Concise: Get to the point
- Current: Update when patterns change
- Friendly: Help future contributors (including future you!)
Last Updated: 2025-01-27
Maintainer: The Human Pattern Lab / SCMS
Status: Living Document 🦊
"The hallway—er, bridge—exists, serves its purpose, and disappears." 🌉