* chore(#604): rename get-shit-done/ runtime directory to gsd-core/ Renames the installed runtime directory `get-shit-done/` to `gsd-core/` so the on-disk name matches the package (`@opengsd/gsd-core`), repo, and binary (`gsd-tools`). The npm package name and binary are unchanged; npx/npm consumers are unaffected. Mechanical (bulk, ~90% of the diff): - `git mv get-shit-done gsd-core` - Swept path/identifier references across the repo via `perl -pe 's/get-shit-done(?!-\w)/gsd-core/g'`. The negative lookahead preserves the five legitimate slug variants that are NOT the directory: get-shit-done-{OLD,cc,classic,cli,redux} (old package/repo names). - Build/manifest wiring: package.json (bin, files, coverage globs), tsconfig.build.json (outDir), ~86 .gitignore build-output entries, stryker.config.mjs, scan-ignore files, install.js path strings. - Frozen (not rewritten): CHANGELOG.md history; translated docs (README.<locale>.md and docs/{ja-JP,ko-KR,pt-BR,zh-CN}/). New logic (review here): - src/installer-migrations/003-rename-get-shit-done-to-gsd-core.cts: a proper ADR-0008 installer migration. On upgrade it walks the legacy `~/.claude/get-shit-done/` tree, classifies each file via the prior install manifest, and emits remove-managed / backup-and-remove for managed files while PRESERVING unknown user-added files. Symlink-safe (skips a symlinked root and symlinked entries; bounds-checks every path under configDir). The framework rolls back on install failure. Emptied dirs may remain (framework has no recursive dir-removal primitive) — documented. - scripts/lint-legacy-dir-name.cjs: CI regression guard forbidding the bare `get-shit-done` directory token (split token to avoid self-match; case- insensitive; `(?!-\w)` lookahead allows the slug variants; allowlists CHANGELOG, translated docs, and `gsd-allow-legacy-name` marker lines). Wired into the lint-tests CI job. - Restored scripts/lint-package-identity-drift.cjs detection regexes (the mechanical sweep had wrongly rewritten the old-name patterns it exists to detect) and marked them as intentional legacy references. - TDD tests for the migration and the guard; do.md slash-command guard regex tightened so a `/gsd-core/bin` path segment is not mistaken for a command; changeset + docs/installer-migrations.md row added. Breaking: the installed runtime path moves `~/.claude/get-shit-done/` -> `~/.claude/gsd-core/`. Migration 003 removes the stale legacy dir's managed files (preserving user files) on upgrade. Users with custom hooks/configs hardcoding the old path must update them. Closes #604 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): unsweep pending changesets + allowlist injection-example docs CI fixes for the rename PR: - Do not sweep pending .changeset/*.md (ephemeral release-note fragments, like CHANGELOG); reverted those body edits so 5 pre-existing malformed fragments (missing type/pr) no longer enter the PR diff and trip docs-lint. Allowlisted .changeset/ in the legacy-name guard accordingly. - Allowlisted TEST-EXAMPLES.md and docs/explanation/security-model.md in prompt-injection-scan.sh: they contain intentional injection examples / security-model prose; the path-reference rewrites are kept. CodeQL alerts on this PR are pre-existing (alert lines unchanged by this PR; none in the new migration/guard) and are out of scope for the rename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): resolve CodeQL alerts surfaced on this PR The rename diff touched files carrying pre-existing CodeQL findings; per the no-pre-existing-dismissal rule, fixing every surfaced alert rather than waving them off. All behavior-preserving: - scripts/ci-test-scope.cjs: build the config-path match from string .includes() instead of a RegExp over an arg-derived value (js/regex-injection). - src/profile-output.cts: escape backslashes before pipe-escaping desc/safeName so the table-cell escape is complete (js/incomplete-sanitization). - tests/{bug-2643,bug-2808,docs-parity-live-registry}: two-pass HTML-comment strip so a bare/unclosed `<!--` cannot survive (js/incomplete-multi-character-sanitization). - tests/inline-plan-threshold: drop the no-op `\s`->`\s` identity replace, keep the meaningful POSIX-class conversion (js/identity-replacement). Verified: build:lib green; the touched test files + ci-test-scope + profile-output suites pass; lint:legacy-name clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): correctly resolve remaining CodeQL alerts (regex-injection + sanitization) The prior commit's fixes for two alerts were ineffective: - ci-test-scope.cjs js/regex-injection: the alert is the CLI-arg-derived `file` reaching static regex `.test(file)` calls (not the config rule). Removed ALL regex over file/t — startsWith/includes/=== string checks + an isWindowsHint helper — so there is no regex sink for the tainted value. - js/incomplete-multi-character-sanitization (3 test files): a single `.replace(/<!--...-->/g,'')` can let `<!--` re-form. Replaced with a fixpoint loop (replace until stable) plus a final bare-opener strip. Verified: no regex over file/t remains; ci-test-scope + the 3 test suites pass; lint:legacy-name clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): make ci-test-scope + comment-strippers regex-free to clear CodeQL CodeQL flags the regex PATTERNS syntactically (regex-injection on the --files arg split; incomplete-multi-character-sanitization on the <!--...--> replace), so loop fixes do not satisfy it. Made these paths regex-free: - ci-test-scope.cjs splitFiles: char-by-char separator tokenizer (no /[,\\s]+/). - 3 test files: indexOf/slice HTML-comment stripper (no .replace(/<!--/)). Behavior preserved; ci-test-scope + the 3 suites pass; guard clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): unblock security base64 scan on the large rename diff The security job hit its 10m timeout: base64-scan.sh choked on the binary test fixture tests/feat-3594-parser-property-style.test.cjs (embedded NUL/ non-UTF8 bytes -> thousands of bogus blobs + "ignored null byte" warnings), and the ~800-file rename diff is slow to scan regardless. - scripts/base64-scan.sh: skip binary-by-content files (grep -Iq .) — they can't carry base64-obfuscated *text* and feeding NUL bytes through the per-line scanner is pathologically slow. collect_files already filtered binary *extensions*; this catches binary *content* in text extensions. - .github/workflows/security-scan.yml: raise the security job timeout 10m->30m to accommodate very large diffs (the scan itself is unchanged). Verified locally: scan skips the fixture, 0 "ignored null byte" warnings, 0 findings, exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): sweep get-shit-done refs introduced by merging next The branch was updated with next (#614/#384/#618 etc.), which reference the get-shit-done/ dir (still named that on next). Swept the stale references in the merged files to gsd-core so the rename stays consistent and lint:legacy-name passes: - commands/gsd/discuss-phase.md (runtime-launcher shim paths) - src/core.cts (getAgentsDir layout comments) - tests/bug-384-agents-runtime-aware.test.cjs (require path to runtime lib) Verified: guard 0 violations; build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): exclude gsd-core/ path segments from bug-3683 command cross-ref invariant The #614 runtime-launcher shim added to discuss-phase.md references `${_GSD_RUNTIME_ROOT}/gsd-core/bin/...`. bug-3683's REF_PATTERN excluded path-y refs only via lookbehind, but `}` precedes `/gsd-core/` in the shim, so it mis-read the directory path as a dangling `/gsd-core` command ref (same class as the #604 bug-2954 fix). Added a trailing `(?![\w-]*\/)` so `/gsd-<x>/...` path segments are not treated as slash-command references. Verified locally on BOTH platforms before pushing: - mac (node 26) full suite: 0 failures - gsd-test-runner (linux, node22 image) full suite: 0 failures - bug-3683 + bug-2954 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): lazily resolve findProjectRoot in gsd-tools (harden flaky CI) CI intermittently failed state.test's gsd-tools subprocess with "findProjectRoot is not a function" (flip-flopping across legs; not reproducible on mac full suite, gsd-test linux full suite, test:unit, or state.test x8). findProjectRoot is a re-export from core.cjs (sourced from project-root.cjs); binding it via destructure at module-load can be undefined under a load-ordering edge. Resolve it lazily at call time via a small wrapper so the lookup happens after core.cjs is fully initialized. Verified green on BOTH platforms before pushing: - mac (node 26) full suite: 0 failures - gsd-test-runner (linux, node22) full suite: 0 failures - state.test.cjs: 106/106; gsd-tools loads cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#604): allowlist verification-patterns.md placeholder examples in secret scan The rename git-mv'd references/verification-patterns.md into gsd-core/, pulling it into the secret-scan diff. It documents stub/placeholder RED-FLAG env-var examples (illustrative Stripe test-key / database-URL / API-key placeholders) — not real credentials. Added it to .secretscanignore with the strict annotation, mirroring the existing gsd-core/workflows/plan-phase.md exception. Verified locally: secret-scan-lint --strict OK; secret-scan --diff origin/next exits 0 with 0 findings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
16 KiB
Verification Patterns
How to verify different types of artifacts are real implementations, not stubs or placeholders.
<core_principle> Existence ≠ Implementation
A file existing does not mean the feature works. Verification must check:
- Exists - File is present at expected path
- Substantive - Content is real implementation, not placeholder
- Wired - Connected to the rest of the system
- Functional - Actually works when invoked
Levels 1-3 can be checked programmatically. Level 4 often requires human verification. </core_principle>
<stub_detection>
Universal Stub Patterns
These patterns indicate placeholder code regardless of file type:
Comment-based stubs:
# Grep patterns for stub comments
grep -E "(TODO|FIXME|XXX|HACK|PLACEHOLDER)" "$file"
grep -E "implement|add later|coming soon|will be" "$file" -i
grep -E "// \.\.\.|/\* \.\.\. \*/|# \.\.\." "$file"
Placeholder text in output:
# UI placeholder patterns
grep -E "placeholder|lorem ipsum|coming soon|under construction" "$file" -i
grep -E "sample|example|test data|dummy" "$file" -i
grep -E "\[.*\]|<.*>|\{.*\}" "$file" # Template brackets left in
Empty or trivial implementations:
# Functions that do nothing
grep -E "return null|return undefined|return \{\}|return \[\]" "$file"
grep -E "pass$|\.\.\.|\bnothing\b" "$file"
grep -E "console\.(log|warn|error).*only" "$file" # Log-only functions
Hardcoded values where dynamic expected:
# Hardcoded IDs, counts, or content
grep -E "id.*=.*['\"].*['\"]" "$file" # Hardcoded string IDs
grep -E "count.*=.*\d+|length.*=.*\d+" "$file" # Hardcoded counts
grep -E "\\\$\d+\.\d{2}|\d+ items" "$file" # Hardcoded display values
</stub_detection>
<react_components>
React/Next.js Components
Existence check:
# File exists and exports component
[ -f "$component_path" ] && grep -E "export (default |)function|export const.*=.*\(" "$component_path"
Substantive check:
# Returns actual JSX, not placeholder
grep -E "return.*<" "$component_path" | grep -v "return.*null" | grep -v "placeholder" -i
# Has meaningful content (not just wrapper div)
grep -E "<[A-Z][a-zA-Z]+|className=|onClick=|onChange=" "$component_path"
# Uses props or state (not static)
grep -E "props\.|useState|useEffect|useContext|\{.*\}" "$component_path"
Stub patterns specific to React:
// RED FLAGS - These are stubs:
return <div>Component</div>
return <div>Placeholder</div>
return <div>{/* TODO */}</div>
return <p>Coming soon</p>
return null
return <></>
// Also stubs - empty handlers:
onClick={() => {}}
onChange={() => console.log('clicked')}
onSubmit={(e) => e.preventDefault()} // Only prevents default, does nothing
Wiring check:
# Component imports what it needs
grep -E "^import.*from" "$component_path"
# Props are actually used (not just received)
# Look for destructuring or props.X usage
grep -E "\{ .* \}.*props|\bprops\.[a-zA-Z]+" "$component_path"
# API calls exist (for data-fetching components)
grep -E "fetch\(|axios\.|useSWR|useQuery|getServerSideProps|getStaticProps" "$component_path"
Functional verification (human required):
- Does the component render visible content?
- Do interactive elements respond to clicks?
- Does data load and display?
- Do error states show appropriately?
</react_components>
<api_routes>
API Routes (Next.js App Router / Express / etc.)
Existence check:
# Route file exists
[ -f "$route_path" ]
# Exports HTTP method handlers (Next.js App Router)
grep -E "export (async )?(function|const) (GET|POST|PUT|PATCH|DELETE)" "$route_path"
# Or Express-style handlers
grep -E "\.(get|post|put|patch|delete)\(" "$route_path"
Substantive check:
# Has actual logic, not just return statement
wc -l "$route_path" # More than 10-15 lines suggests real implementation
# Interacts with data source
grep -E "prisma\.|db\.|mongoose\.|sql|query|find|create|update|delete" "$route_path" -i
# Has error handling
grep -E "try|catch|throw|error|Error" "$route_path"
# Returns meaningful response
grep -E "Response\.json|res\.json|res\.send|return.*\{" "$route_path" | grep -v "message.*not implemented" -i
Stub patterns specific to API routes:
// RED FLAGS - These are stubs:
export async function POST() {
return Response.json({ message: "Not implemented" })
}
export async function GET() {
return Response.json([]) // Empty array with no DB query
}
export async function PUT() {
return new Response() // Empty response
}
// Console log only:
export async function POST(req) {
console.log(await req.json())
return Response.json({ ok: true })
}
Wiring check:
# Imports database/service clients
grep -E "^import.*prisma|^import.*db|^import.*client" "$route_path"
# Actually uses request body (for POST/PUT)
grep -E "req\.json\(\)|req\.body|request\.json\(\)" "$route_path"
# Validates input (not just trusting request)
grep -E "schema\.parse|validate|zod|yup|joi" "$route_path"
Functional verification (human or automated):
- Does GET return real data from database?
- Does POST actually create a record?
- Does error response have correct status code?
- Are auth checks actually enforced?
</api_routes>
<database_schema>
Database Schema (Prisma / Drizzle / SQL)
Existence check:
# Schema file exists
[ -f "prisma/schema.prisma" ] || [ -f "drizzle/schema.ts" ] || [ -f "src/db/schema.sql" ]
# Model/table is defined
grep -E "^model $model_name|CREATE TABLE $table_name|export const $table_name" "$schema_path"
Substantive check:
# Has expected fields (not just id)
grep -A 20 "model $model_name" "$schema_path" | grep -E "^\s+\w+\s+\w+"
# Has relationships if expected
grep -E "@relation|REFERENCES|FOREIGN KEY" "$schema_path"
# Has appropriate field types (not all String)
grep -A 20 "model $model_name" "$schema_path" | grep -E "Int|DateTime|Boolean|Float|Decimal|Json"
Stub patterns specific to schemas:
// RED FLAGS - These are stubs:
model User {
id String @id
// TODO: add fields
}
model Message {
id String @id
content String // Only one real field
}
// Missing critical fields:
model Order {
id String @id
// No: userId, items, total, status, createdAt
}
Wiring check:
# Migrations exist and are applied
ls prisma/migrations/ 2>/dev/null | wc -l # Should be > 0
npx prisma migrate status 2>/dev/null | grep -v "pending"
# Client is generated
[ -d "node_modules/.prisma/client" ]
Functional verification:
# Can query the table (automated)
npx prisma db execute --stdin <<< "SELECT COUNT(*) FROM $table_name"
</database_schema>
<hooks_utilities>
Custom Hooks and Utilities
Existence check:
# File exists and exports function
[ -f "$hook_path" ] && grep -E "export (default )?(function|const)" "$hook_path"
Substantive check:
# Hook uses React hooks (for custom hooks)
grep -E "useState|useEffect|useCallback|useMemo|useRef|useContext" "$hook_path"
# Has meaningful return value
grep -E "return \{|return \[" "$hook_path"
# More than trivial length
[ $(wc -l < "$hook_path") -gt 10 ]
Stub patterns specific to hooks:
// RED FLAGS - These are stubs:
export function useAuth() {
return { user: null, login: () => {}, logout: () => {} }
}
export function useCart() {
const [items, setItems] = useState([])
return { items, addItem: () => console.log('add'), removeItem: () => {} }
}
// Hardcoded return:
export function useUser() {
return { name: "Test User", email: "test@example.com" }
}
Wiring check:
# Hook is actually imported somewhere
grep -r "import.*$hook_name" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path"
# Hook is actually called
grep -r "$hook_name()" src/ --include="*.tsx" --include="*.ts" | grep -v "$hook_path"
</hooks_utilities>
<environment_config>
Environment Variables and Configuration
Existence check:
# .env file exists
[ -f ".env" ] || [ -f ".env.local" ]
# Required variable is defined
grep -E "^$VAR_NAME=" .env .env.local 2>/dev/null
Substantive check:
# Variable has actual value (not placeholder)
grep -E "^$VAR_NAME=.+" .env .env.local 2>/dev/null | grep -v "your-.*-here|xxx|placeholder|TODO" -i
# Value looks valid for type:
# - URLs should start with http
# - Keys should be long enough
# - Booleans should be true/false
Stub patterns specific to env:
# RED FLAGS - These are stubs:
DATABASE_URL=your-database-url-here
STRIPE_SECRET_KEY=sk_test_xxx
API_KEY=placeholder
NEXT_PUBLIC_API_URL=http://localhost:3000 # Still pointing to localhost in prod
Wiring check:
# Variable is actually used in code
grep -r "process\.env\.$VAR_NAME|env\.$VAR_NAME" src/ --include="*.ts" --include="*.tsx"
# Variable is in validation schema (if using zod/etc for env)
grep -E "$VAR_NAME" src/env.ts src/env.mjs 2>/dev/null
</environment_config>
<wiring_verification>
Wiring Verification Patterns
Wiring verification checks that components actually communicate. This is where most stubs hide.
Pattern: Component → API
Check: Does the component actually call the API?
# Find the fetch/axios call
grep -E "fetch\(['\"].*$api_path|axios\.(get|post).*$api_path" "$component_path"
# Verify it's not commented out
grep -E "fetch\(|axios\." "$component_path" | grep -v "^.*//.*fetch"
# Check the response is used
grep -E "await.*fetch|\.then\(|setData|setState" "$component_path"
Red flags:
// Fetch exists but response ignored:
fetch('/api/messages') // No await, no .then, no assignment
// Fetch in comment:
// fetch('/api/messages').then(r => r.json()).then(setMessages)
// Fetch to wrong endpoint:
fetch('/api/message') // Typo - should be /api/messages
Pattern: API → Database
Check: Does the API route actually query the database?
# Find the database call
grep -E "prisma\.$model|db\.query|Model\.find" "$route_path"
# Verify it's awaited
grep -E "await.*prisma|await.*db\." "$route_path"
# Check result is returned
grep -E "return.*json.*data|res\.json.*result" "$route_path"
Red flags:
// Query exists but result not returned:
await prisma.message.findMany()
return Response.json({ ok: true }) // Returns static, not query result
// Query not awaited:
const messages = prisma.message.findMany() // Missing await
return Response.json(messages) // Returns Promise, not data
Pattern: Form → Handler
Check: Does the form submission actually do something?
# Find onSubmit handler
grep -E "onSubmit=\{|handleSubmit" "$component_path"
# Check handler has content
grep -A 10 "onSubmit.*=" "$component_path" | grep -E "fetch|axios|mutate|dispatch"
# Verify not just preventDefault
grep -A 5 "onSubmit" "$component_path" | grep -v "only.*preventDefault" -i
Red flags:
// Handler only prevents default:
onSubmit={(e) => e.preventDefault()}
// Handler only logs:
const handleSubmit = (data) => {
console.log(data)
}
// Handler is empty:
onSubmit={() => {}}
Pattern: State → Render
Check: Does the component render state, not hardcoded content?
# Find state usage in JSX
grep -E "\{.*messages.*\}|\{.*data.*\}|\{.*items.*\}" "$component_path"
# Check map/render of state
grep -E "\.map\(|\.filter\(|\.reduce\(" "$component_path"
# Verify dynamic content
grep -E "\{[a-zA-Z_]+\." "$component_path" # Variable interpolation
Red flags:
// Hardcoded instead of state:
return <div>
<p>Message 1</p>
<p>Message 2</p>
</div>
// State exists but not rendered:
const [messages, setMessages] = useState([])
return <div>No messages</div> // Always shows "no messages"
// Wrong state rendered:
const [messages, setMessages] = useState([])
return <div>{otherData.map(...)}</div> // Uses different data
</wiring_verification>
<verification_checklist>
Quick Verification Checklist
For each artifact type, run through this checklist:
Component Checklist
- File exists at expected path
- Exports a function/const component
- Returns JSX (not null/empty)
- No placeholder text in render
- Uses props or state (not static)
- Event handlers have real implementations
- Imports resolve correctly
- Used somewhere in the app
API Route Checklist
- File exists at expected path
- Exports HTTP method handlers
- Handlers have more than 5 lines
- Queries database or service
- Returns meaningful response (not empty/placeholder)
- Has error handling
- Validates input
- Called from frontend
Schema Checklist
- Model/table defined
- Has all expected fields
- Fields have appropriate types
- Relationships defined if needed
- Migrations exist and applied
- Client generated
Hook/Utility Checklist
- File exists at expected path
- Exports function
- Has meaningful implementation (not empty returns)
- Used somewhere in the app
- Return values consumed
Wiring Checklist
- Component → API: fetch/axios call exists and uses response
- API → Database: query exists and result returned
- Form → Handler: onSubmit calls API/mutation
- State → Render: state variables appear in JSX
</verification_checklist>
<automated_verification_script>
Automated Verification Approach
For the verification subagent, use this pattern:
# 1. Check existence
check_exists() {
[ -f "$1" ] && echo "EXISTS: $1" || echo "MISSING: $1"
}
# 2. Check for stub patterns
check_stubs() {
local file="$1"
local stubs=$(grep -c -E "TODO|FIXME|placeholder|not implemented" "$file" 2>/dev/null || echo 0)
[ "$stubs" -gt 0 ] && echo "STUB_PATTERNS: $stubs in $file"
}
# 3. Check wiring (component calls API)
check_wiring() {
local component="$1"
local api_path="$2"
grep -q "$api_path" "$component" && echo "WIRED: $component → $api_path" || echo "NOT_WIRED: $component → $api_path"
}
# 4. Check substantive (more than N lines, has expected patterns)
check_substantive() {
local file="$1"
local min_lines="$2"
local pattern="$3"
local lines=$(wc -l < "$file" 2>/dev/null || echo 0)
local has_pattern=$(grep -c -E "$pattern" "$file" 2>/dev/null || echo 0)
[ "$lines" -ge "$min_lines" ] && [ "$has_pattern" -gt 0 ] && echo "SUBSTANTIVE: $file" || echo "THIN: $file ($lines lines, $has_pattern matches)"
}
Run these checks against each must-have artifact. Aggregate results into VERIFICATION.md.
</automated_verification_script>
<human_verification_triggers>
When to Require Human Verification
Some things can't be verified programmatically. Flag these for human testing:
Always human:
- Visual appearance (does it look right?)
- User flow completion (can you actually do the thing?)
- Real-time behavior (WebSocket, SSE)
- External service integration (Stripe, email sending)
- Error message clarity (is the message helpful?)
- Performance feel (does it feel fast?)
Human if uncertain:
- Complex wiring that grep can't trace
- Dynamic behavior depending on state
- Edge cases and error states
- Mobile responsiveness
- Accessibility
Format for human verification request:
## Human Verification Required
### 1. Chat message sending
**Test:** Type a message and click Send
**Expected:** Message appears in list, input clears
**Check:** Does message persist after refresh?
### 2. Error handling
**Test:** Disconnect network, try to send
**Expected:** Error message appears, message not lost
**Check:** Can retry after reconnect?
</human_verification_triggers>
<checkpoint_automation_reference>
Pre-Checkpoint Automation
For automation-first checkpoint patterns, server lifecycle management, CLI installation handling, and error recovery protocols, see:
@~/.claude/gsd-core/references/checkpoints.md → <automation_reference> section
Key principles:
- Claude sets up verification environment BEFORE presenting checkpoints
- Users never run CLI commands (visit URLs only)
- Server lifecycle: start before checkpoint, handle port conflicts, keep running for duration
- CLI installation: auto-install where safe, checkpoint for user choice otherwise
- Error handling: fix broken environment before checkpoint, never present checkpoint with failed setup
</checkpoint_automation_reference>