Development Workflow
This guide covers the standard development workflow for the Sustentus monorepo.
Daily Development Workflow
1. Start Development Environment
# Pull latest changes
git pull origin main
# Install any new dependencies
pnpm install
# Start packages in watch mode
pnpm packages:dev2. Start Relevant Applications
# Start the app you're working on
pnpm marketing:dev # Marketing site
pnpm docs:dev # Documentation3. Make Changes
- Edit code in your editor
- Changes hot-reload automatically
- Check browser for updates
4. Verify Changes
# Run linting
pnpm lint
# Format code
pnpm format
# Build to ensure no errors
pnpm build5. Commit Changes
Make small, atomic commits as you go. The branch, PR, and merge are driven by the delivery pipeline — you commit; the Build and Release stages handle the rest.
# Stage changes
git add .
# Commit with a descriptive message (the pre-commit hook auto-formats staged files)
git commit -m "feat: add new feature"Feature delivery pipeline
Sustentus does not ship features through an ad-hoc “branch → commit → PR → merge” flow. Features
move through an ICM delivery pipeline driven by a single /pipeline command, through four
ordered, human-gated stages. Nothing advances automatically —
you review (and edit) a stage’s output, then invoke the next stage. That pause is the gate.
The pipeline lives in the repo at .icm/CONTEXT.md (the workspace map) and
.claude/skills/pipeline/SKILL.md (the router). Each stage’s full contract is
.icm/stages/NN_*/CONTEXT.md.
| Stage | Command | What it produces | Gate before moving on |
|---|---|---|---|
| Scope | /pipeline scope <input> | The source recorded as received; the scope settled with you in session into scope.md; the intake stubs cut — all pushed to main | ✅ you review scope.md and the batch on main, then run new |
| Define | /pipeline new | A spec (canonical, in git) and a draft PR, one per intake stub | ✅ you tick Spec approved on the PR |
| Build | /pipeline build <slug> | Implementation on the run’s branch; PR flips draft → open and pushes — the previews build on that push (drafts are blind) | ✅ you smoke the post-flip previews, tick Ready to merge |
| Release | /pipeline release <slug> | Reviews · docs + changelog + close-out in-PR → squash-merge; CI announces | — (the merge ends the run) |
(The former Verify and Ship stages were merged into Release in 2026-08: your
Ready to merge tick now attests your own preview smoke-testing, and only a blocking CI
failure, a security-critical finding, or deploy-breaking config can hold the merge after it —
every other finding is parked as a stub in .icm/intake/triage/ and picked up by a fast lane
later.)
Work arrives in any medium — a written user story, a prototype URL, a document, a prompt written after a call, a Slack thread. Scope records it as received (text verbatim, anything else by link), reads the business docs and the codebase to understand what is being asked, and settles it with you in session in rounds of questions; anything you cannot settle is written down as open for Define, never assumed. There is no question sheet and no out-of-band answering.
Two of those gates are PR checkboxes — Spec approved and Ready to merge — and both are yours to tick. The business’s involvement ends when the scope is settled at Scope; from Define onward every approval is yours.
Fast lanes
Work that needs no scope and no spec skips the front entirely. A lane is one agent invocation
that ends with a PR you can merge yourself: it fixes, opens the draft PR, reads the draft head’s
verdict (nothing is owed — drafts run no CI and build no previews), then — while the PR is still draft — writes the changelog page when the change is
user-visible (bug/tweak — never for a chore) and runs the close-out on the branch, and only then
pushes, flips the PR ready, waits for the full gate and stops. The flip is the last act: it is
what makes the PR mergeable, so nothing the lane still owes comes after it. The PR body carries no checkbox — you smoke-test the previews the lane
reported and press squash-merge in GitHub; the merge button is the gate. Nothing is left for
a second invocation, and the agent never merges a lane PR. The contracts live in
.icm/lanes/<name>/CONTEXT.md.
| Command | For |
|---|---|
/pipeline bug "<report>" | reproduce → fix → green PR (+ changelog if user-visible) |
/pipeline tweak "<change>" | a tiny, fully-specified adjustment → small green PR |
/pipeline chore "<task>" | refactor / dep-bump / migration — no behaviour change, no changelog |
/pipeline knowledge add|edit|remove "<what>" | one page of this site’s business or technical section changed outside a release — a docs-only PR, no run, no changelog |
knowledge is the one sanctioned way to change project knowledge — the business and technical
pages here, which every stage reads through .icm/_shared/knowledge-map.md — when no shipped
change is behind it: the agent routes the request to one page through the map, edits it under the
docs-sync skill’s rules, updates the map if a page was added or removed, and opens a docs-only
PR for you to merge. A lane that turns out to carry product decisions gets routed back to
/pipeline scope. Lanes can
also start from a parked finding by name — .icm/intake/triage/ holds one small stub per
off-ticket finding any stage cut instead of widening its PR (/pipeline bug <stub-name> picks it
up). After your merge, release.yaml posts the announcement (if a changelog page rode along) and
checks the archived run landed.
The PR and the run spec are the source of truth
Define writes the spec to .icm/runs/<slug>/02_define/output/spec.md (the canonical, editable
spec) and opens a draft PR whose body links to it — no GitHub issue is created. The PR’s labels,
including a single stage:* label (stage:define → stage:build → stage:release),
are projected from the spec and from which run outputs exist, so the PR board always shows exactly
where each feature is. The two human gates are checkboxes in the PR body, ticked by a human — never
by the agent.
Branch management
There is no develop branch and no manual branch-naming convention to memorise. The tooling names
the branch (e.g. claude/<slug>-<hash>) and the run’s run.md records it. main is the only
long-lived branch and is always production-ready — Release squash-merges into it and deploys.
Commits and PRs
- Keep commits small and atomic. Conventional-commit prefixes (
feat:,fix:,refactor:,chore:,docs:) are preferred but not enforced — the repo’sCONVENTIONS.mdholds the canonical git rules. - The Husky + lint-staged pre-commit hook auto-formats staged
*.{ts,tsx,md}files, so formatting is handled for you. - You don’t open or merge the PR by hand — Define opens the draft PR, Build flips it to open (then pushes, so the advisory Quality job and the affected product-app previews land — draft pushes run no CI and build no previews), and Release merges it, but only after you tick Ready to merge on the PR.
Docs stay in sync automatically
- Release updates the technical and business/product docs in
apps/docsin the same PR as the change (via thedocs-syncskill), so this site never drifts from the code, and publishes the changelog page with the merge — the one release artifact (its frontmatter carries the announcement’s title, summary and audience). - Release also archives the run on the branch (
close-out.sh, the last commit before the merge), so the squash-merge publishes the record with the change. On the merge, therelease.yamlworkflow posts that announcement to#product-updateand checks the archive landed — the agent never sends the announcement by hand, and there is no deploy-status gate.
Working with Packages
UI Package Development
-
Start UI in watch mode:
pnpm ui:dev -
Edit component in
packages/ui/src/components/ -
Test in app by running a consuming app, e.g.
pnpm web:devorpnpm marketing:dev
No story is needed for a new component — Storybook (apps/storybook) was archived on 2026-09-23.
Services Package Development
-
Start services in watch mode:
pnpm services:dev -
Edit service in
packages/services/src/ -
Use in apps - they’ll pick up changes automatically
-
Test with unit tests (when implemented)
Working with Applications
Adding New Page
In Marketing App
-
Create page file:
# For route /about touch apps/marketing/app/about/page.tsx -
Implement page:
export default function AboutPage() { return <div>About</div>; } -
Add to navigation if needed
-
Test at
http://localhost:3001/about
In Documentation App
-
Create MDX file:
touch apps/docs/app/guides/new-guide/page.mdx -
Write content:
# New Guide Guide content here... -
Appears automatically in navigation
Adding New Component
-
Create component file:
touch apps/marketing/components/new-component.tsx -
Implement component:
type Props = { title: string; }; export const NewComponent = ({ title }: Props) => <div>{title}</div>; -
Import and use:
import { NewComponent } from "@/components/new-component";
Dependency Management
Adding Dependencies
# Add to specific package
cd apps/marketing
pnpm add package-name
# Add to UI package
cd packages/ui
pnpm add package-name
# Add dev dependency
pnpm add -D package-nameUpdating Dependencies
# Update specific package
pnpm update package-name
# Update all in workspace
pnpm update -r
# Check outdated
pnpm outdatedRemoving Dependencies
cd apps/marketing
pnpm remove package-nameCode review and merge (the Release stage)
Review and merge are part of the pipeline, not a separate manual ritual. After Build hands over a
green PR, you smoke-test the Vercel preview and tick Ready to merge — the tick is the
attestation of that manual testing. Release then does the rest in one stage; its full contract
is .icm/stages/04_release/CONTEXT.md.
What Release handles, so the change merges sound:
- CI green — format, lint, typecheck, tests, and the Vercel preview builds, read as one settled verdict; Release refuses to merge on red or unsettled CI.
- Reviews — the code review (plus security / production-readiness passes when the diff
touches auth, payments, data, or env vars). Findings are triaged, not litigated: trivial
in-ticket fixes land on the branch; a security-critical or deploy-breaking finding stops the
merge; everything else is parked as a stub in
.icm/intake/triage/and the merge proceeds. - Docs + changelog — the affected
apps/docspages and the one changelog page are updated in the same PR (docs-sync,changelog-entry), so this site never lags the code. - The merge — one squash-merge on the ticked box, after
mainhas been merged into the branch and the run’s close-out has been committed and pushed on the branch as the last commit. The Slack announcement then runs itself in CI (release.yaml), which also checks the archived run landed.
When review comments come in, they are triaged by the same rule — follow-up commits on the same run branch for in-ticket fixes, triage stubs for the rest.
Testing Strategies
Manual Testing
- Visual testing: Check UI in browser
- Functional testing: Test user interactions
- Responsive testing: Different screen sizes
- Cross-browser testing: Chrome, Firefox, Safari
- Accessibility testing: Keyboard navigation, screen readers
Automated testing
Unit tests run on Vitest, co-located with the code they test; the end-to-end tier is the Playwright
persona walk in apps/e2e. The Quality workflow runs the unit suite on every pull request. Where
tests live, what to test and what not to: CONVENTIONS.md → Testing.
Development Best Practices
Code Organization
- Logical grouping: Group related files
- Clear naming: Descriptive file and function names
- Consistent structure: Follow existing patterns
- Component size: Keep components focused
TypeScript
- Type everything: Avoid
any - Infer types: Use type inference where possible
- Shared types: Put shared types in packages
- Strict mode: Keep TypeScript strict
React
- Functional components: Use arrow functions
- Named exports: Prefer named over default exports
- Composition: Compose components from smaller ones
- Server components: Default to server components in Next.js
Styling
- Tailwind first: Use Tailwind utilities
- Consistent spacing: Use Tailwind spacing scale
- Responsive: Mobile-first approach
- Dark mode: Support both themes
Performance
- Code splitting: Use dynamic imports
- Image optimization: Use Next.js Image
- Bundle size: Monitor bundle size
- Lazy loading: Load components when needed
Debugging
Common Issues
Hot Reload Not Working
# Restart dev server
# Ctrl+C to stop
pnpm devBuild Errors
# Clean and rebuild
pnpm clean
pnpm buildType Errors
# Rebuild packages
pnpm packages:build
# Restart TypeScript server in VSCode
# Cmd+Shift+P > TypeScript: Restart TS ServerDebugging Tools
- Browser DevTools: Console, Network, Elements
- React DevTools: Component hierarchy, props
- VSCode Debugger: Breakpoints, step through code
- Turbo:
turbo run build --dry-runto see task graph
Quick Reference
Common Commands
# Development
pnpm dev # All apps
pnpm marketing:dev # Marketing app
pnpm docs:dev # Documentation
pnpm packages:dev # Packages watch mode
# Building
pnpm build # Build all
pnpm marketing:build # Build marketing
pnpm packages:build # Build packages
# Code Quality
pnpm lint # Lint all
pnpm format # Format all
pnpm clean # Clean builds
# Delivery pipeline (/pipeline command)
/pipeline scope <input> # Scope: record the source, settle the scope in session, cut the intake stubs
/pipeline new # Define: next stub → spec + draft PR (gated by the Spec approved tick)
/pipeline build <slug> # Build: implement on the run's branch, flip PR draft → open
/pipeline release <slug> # Release: reviews · docs + changelog → merge (gated by the Ready to merge tick)
# Fast lanes (one invocation → a green PR you squash-merge from GitHub; no checkbox)
/pipeline bug "<report>" # Reproduce → fix → draft PR → (+ changelog) → close-out → flip ready
/pipeline tweak "<change>" # Tiny adjustment → draft PR → close-out → flip ready
/pipeline chore "<task>" # Refactor / dep-bump / migration → draft PR → close-out → flip ready
# Knowledge (this site's business/technical pages, outside a release; docs-only PR, no run)
/pipeline knowledge add|edit|remove "<what>" # Route via the knowledge map → change the page → PRDirectory Navigation
# Root
cd ~/Developer/sustentus
# Apps
cd apps/marketing
cd apps/docs
# Packages
cd packages/ui
cd packages/servicesError tracking
apps/web reports errors to Sentry; no other app does. Which errors are captured, the three
environment variables and where to look are on Error tracking.