webproxy/.claude/hooks/unicorn-boot.ts
Jeremy Meyer 17bba2d040 feat: initial monorepo setup with Next.js landing page
- pnpm workspaces monorepo with apps/ and packages/
- Next.js 16 landing page (apps/web) with dark theme, feature overview
- Package stubs: @webproxy/core, @webproxy/indexer, @webproxy/shared
- Proxy server placeholder (apps/proxy)
- Project spec, architecture docs, and deployment guide
- Gitea remote configured at 185.191.239.154:3000

Co-Authored-By: UnicornDev <noreply@unicorndev.wtf>
2026-02-26 18:24:28 -08:00

397 lines
14 KiB
TypeScript
Executable File

#!/usr/bin/env npx tsx
/**
* @file unicorn-boot.ts
* @description SessionStart hook for UnicornDev framework initialization
* @layer Infrastructure
*
* Provides:
* - Global repo health overview (for awareness)
* - Prompt scoping workflow for per-task analysis
* - Quality thresholds that apply to SCOPED analysis, not global counts
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync } from 'fs'
import { join, basename, extname } from 'path'
import { execSync } from 'child_process'
// =============================================================================
// Types
// =============================================================================
interface Settings {
version?: string
techLevel?: string
proactivityLevel?: string
interactionLevel?: string
autonomy?: {
mode?: string
selfRecover?: boolean
continueUntilDone?: boolean
askUser?: string
}
quality?: {
minQaScore?: number
requireSpecs?: boolean
requireTests?: boolean
thresholds?: QualityThresholds
}
metaCognition?: { enabled?: boolean }
learning?: { enabled?: boolean }
execution?: { checkpointInterval?: number }
autoWorkflow?: {
autoCommit?: boolean
commitStyle?: string
}
workflow?: {
type?: string
gitFlow?: string
integrationBranch?: string
branchPrefix?: string
commitStrategy?: string
commitDirectly?: boolean
pushAfterCommit?: boolean
createPR?: boolean
qaRequired?: boolean
specsRequired?: boolean
testRequired?: boolean
commitPerTask?: boolean
qaLoop?: { enabled?: boolean; testLocally?: boolean }
recommendations?: Record<string, boolean>
}
}
interface QualityThresholds {
// Scoped thresholds (applied to prompt scope, not global)
scopedSpecCoverage: number // % of scoped features with specs (default: 80)
scopedImplComplete: number // % of spec criteria implemented (default: 70)
scopedTestCoverage: number // % test coverage for scoped files (default: 60)
scopedQaScore: number // Min QA score for scoped files (default: 80)
}
const DEFAULT_THRESHOLDS: QualityThresholds = {
scopedSpecCoverage: 80, // 80% of touched features should have specs
scopedImplComplete: 70, // 70% of spec criteria should be implemented
scopedTestCoverage: 60, // 60% test coverage for modified files
scopedQaScore: 80 // QA score 80+ for modified files
}
interface RepoOverview {
specCount: number
codeFileCount: number
testFileCount: number
structure: string
}
// =============================================================================
// Utilities
// =============================================================================
function getProjectRoot(): string {
return process.env.CLAUDE_PROJECT_DIR || process.cwd()
}
function safeReadJson<T>(path: string, defaultValue: T): T {
try {
if (existsSync(path)) {
return JSON.parse(readFileSync(path, 'utf-8'))
}
} catch { /* ignore */ }
return defaultValue
}
function getGitInfo(): { branch: string; status: string; flow: string } {
try {
const branch = execSync('git branch --show-current 2>/dev/null', { encoding: 'utf-8' }).trim() || 'unknown'
const statusOutput = execSync('git status --short 2>/dev/null', { encoding: 'utf-8' }).trim()
const lines = statusOutput.split('\n').filter(Boolean)
const status = lines.length > 0 ? `${lines.length} changed` : 'clean'
const branchList = execSync('git branch -a 2>/dev/null', { encoding: 'utf-8' })
let flow = 'trunk'
if (/\bdevelop\b|\bdev\b/.test(branchList)) flow = 'gitflow'
else if (/feature\/|feat\/|fix\//.test(branchList)) flow = 'github-flow'
return { branch, status, flow }
} catch {
return { branch: 'unknown', status: 'unknown', flow: 'trunk' }
}
}
function findFiles(dir: string, extensions: string[], maxDepth = 4): string[] {
const results: string[] = []
const excludeDirs = ['node_modules', 'dist', '.next', 'coverage', '__tests__', '.git']
function walk(currentDir: string, depth: number): void {
if (depth > maxDepth) return
try {
const entries = readdirSync(currentDir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = join(currentDir, entry.name)
if (entry.isDirectory()) {
if (!excludeDirs.includes(entry.name) && !entry.name.startsWith('.')) {
walk(fullPath, depth + 1)
}
} else if (entry.isFile()) {
if (extensions.includes(extname(entry.name))) {
results.push(fullPath)
}
}
}
} catch { /* ignore */ }
}
if (existsSync(dir)) walk(dir, 0)
return results
}
// =============================================================================
// Repo Overview (Quick Global Stats)
// =============================================================================
function getRepoOverview(projectRoot: string): RepoOverview {
let specCount = 0
let codeFileCount = 0
let testFileCount = 0
let structure = 'unknown'
// Count specs
for (const dir of ['specs', 'docs'].map(d => join(projectRoot, d))) {
specCount += findFiles(dir, ['.md'], 3).length
}
// Count code and tests
for (const dir of ['src', 'app', 'packages', 'lib'].map(d => join(projectRoot, d))) {
const files = findFiles(dir, ['.ts', '.tsx'], 4)
for (const f of files) {
if (f.includes('.test.') || f.includes('.spec.')) {
testFileCount++
} else {
codeFileCount++
}
}
}
// Detect structure
const pkgPath = join(projectRoot, 'package.json')
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
if (deps['next']) structure = existsSync(join(projectRoot, 'app')) ? 'nextjs-app' : 'nextjs-pages'
else if (deps['react']) structure = 'react'
} catch { /* ignore */ }
}
if (existsSync(join(projectRoot, 'packages')) || existsSync(join(projectRoot, 'apps'))) {
structure = 'monorepo'
}
return { specCount, codeFileCount, testFileCount, structure }
}
// =============================================================================
// Logging
// =============================================================================
function logSessionStart(logsDir: string, branch: string): void {
const logFile = join(logsDir, `session-${new Date().toISOString().split('T')[0]}.jsonl`)
appendFileSync(logFile, JSON.stringify({
timestamp: new Date().toISOString(),
event: 'session_start',
branch,
type: 'SessionStart_hook'
}) + '\n')
}
function initializeMetaState(logsDir: string): void {
writeFileSync(join(logsDir, 'meta-state.json'), JSON.stringify({
sessionStart: new Date().toISOString(),
toolCalls: 0,
searchPatterns: {},
errorPatterns: {},
filesAccessed: [],
lastCheckpoint: 0,
warnings: [],
bootedVia: 'SessionStart_hook'
}, null, 2))
}
// =============================================================================
// Main
// =============================================================================
function main(): void {
const projectRoot = getProjectRoot()
const logsDir = join(projectRoot, '.claude/logs')
mkdirSync(logsDir, { recursive: true })
const settings = safeReadJson<Settings>(join(projectRoot, '.claude/settings.json'), {})
const knowledge = safeReadJson<{ entries?: Record<string, unknown> }>(
join(projectRoot, '.claude/knowledge/index.json'),
{ entries: {} }
)
const knowledgeCount = Object.keys(knowledge.entries || {}).length
const git = getGitInfo()
const repo = getRepoOverview(projectRoot)
logSessionStart(logsDir, git.branch)
initializeMetaState(logsDir)
// Settings
const techLevel = settings.techLevel || 'relaxed'
const techScales: Record<string, number> = { hardcore: 5, strict: 4, relaxed: 3, lenient: 2, sloppy: 1 }
const techScale = techScales[techLevel] || 3
const proactivity = settings.proactivityLevel || 'autonomous'
const proactivityScales: Record<string, number> = { autonomous: 5, proactive: 4, careful: 3, cautious: 2, paranoid: 1 }
const proactivityScale = proactivityScales[proactivity] || 5
const minQaScore = settings.quality?.minQaScore || 85
const requireSpecs = settings.quality?.requireSpecs !== false
const workflowType = settings.workflow?.type || 'spec-first'
const commitPerTask = settings.workflow?.commitPerTask !== false
const autoCommit = settings.autoWorkflow?.autoCommit !== false
const commitStyle = settings.autoWorkflow?.commitStyle || 'conventional'
const autonomyMode = settings.autonomy?.mode || 'full'
const selfRecover = settings.autonomy?.selfRecover !== false
const checkpoint = settings.execution?.checkpointInterval || 10
// Git workflow settings
const gitFlow = settings.workflow?.gitFlow || git.flow
const integrationBranch = settings.workflow?.integrationBranch || (gitFlow === 'gitflow' ? 'develop' : 'main')
const branchPrefix = settings.workflow?.branchPrefix || 'feat/'
const commitStrategy = settings.workflow?.commitStrategy || commitStyle
const commitDirectly = settings.workflow?.commitDirectly ?? (gitFlow === 'trunk')
const pushAfterCommit = settings.workflow?.pushAfterCommit ?? settings.autoWorkflow?.pushAfterCommit ?? false
const createPR = settings.workflow?.createPR ?? false
const qaRequired = settings.workflow?.qaRequired !== false
const specsRequired = settings.workflow?.specsRequired !== false
const testRequired = settings.workflow?.testRequired ?? false
const recommendations = settings.workflow?.recommendations ?? {}
// Build lifecycle string
const lifecycle = [
specsRequired ? 'spec' : null,
!commitDirectly ? 'branch' : null,
'implement',
qaRequired ? 'QA' : null,
'commit',
pushAfterCommit ? 'push' : null,
createPR ? 'PR' : null,
].filter(Boolean).join(' → ')
// Build recommendations string
const recsActive = Object.entries(recommendations).filter(([, v]) => v).map(([k]) => {
const labels: Record<string, string> = { featureBranch: 'feature branches', fullQA: 'full QA', specFirst: 'spec-first', codeReview: 'code review' }
return labels[k] || k
})
// Thresholds (scoped, not global)
const thresholds: QualityThresholds = { ...DEFAULT_THRESHOLDS, ...settings.quality?.thresholds }
const context = `🦄 UnicornDev Boot
## CONFIG
| Setting | Value |
|---------|-------|
| tech | ${techLevel} (${techScale}/5) |
| proactive | ${proactivity} (${proactivityScale}/5) |
| qa | ≥${minQaScore} (${Math.round(minQaScore/20)}/5) |
| workflow | ${workflowType} |
**Autonomy:** mode=${autonomyMode} self-recover=${selfRecover ? 'on' : 'off'}
**State:** branch=\`${git.branch}\` git=${git.status} flow=${git.flow}
**Commits:** ${autoCommit ? commitStyle : 'manual'}${commitPerTask ? ' (per-task)' : ''}
## REPO OVERVIEW
- Structure: ${repo.structure}
- Specs: ${repo.specCount} | Code: ${repo.codeFileCount} | Tests: ${repo.testFileCount}
- Knowledge entries: ${knowledgeCount}
## GIT WORKFLOW
| Setting | Value |
|---------|-------|
| flow | ${gitFlow} |
| integration | \`${integrationBranch}\` |
| branch prefix | \`${branchPrefix}\` |
| commit | ${commitDirectly ? 'direct to branch' : 'feature branch required'} |
| strategy | ${commitStrategy} |
| push | ${pushAfterCommit ? 'auto after commit' : 'manual'} |
| PR | ${createPR ? 'auto-create to ' + integrationBranch : 'manual'} |
| QA gate | ${qaRequired ? 'required' : 'optional'} |
**Task Lifecycle:** ${lifecycle}${recsActive.length > 0 ? `\n**Recommendations:** ${recsActive.join(', ')}` : ''}
## QUALITY THRESHOLDS (Applied to Prompt Scope)
| Metric | Threshold | Description |
|--------|-----------|-------------|
| Spec Coverage | ≥${thresholds.scopedSpecCoverage}% | Features in scope must have specs |
| Impl Complete | ≥${thresholds.scopedImplComplete}% | Spec criteria implemented |
| Test Coverage | ≥${thresholds.scopedTestCoverage}% | Tests for modified files |
| QA Score | ≥${thresholds.scopedQaScore} | Quality score for scope |
---
## WORKFLOW: Prompt Scoping Protocol
### PHASE 1: SCOPE THE PROMPT
Before any code, determine what the prompt touches:
\`\`\`
PROMPT SCOPE ANALYSIS
├─ Features: [list features/modules involved]
├─ Files: [list files likely to be modified]
├─ Dependencies: [upstream/downstream impacts]
└─ Boundaries: [what's IN vs OUT of scope]
\`\`\`
### PHASE 2: SCOPED QUALITY CHECK
For the scoped features/files, assess:
\`\`\`
SCOPED ANALYSIS
├─ Spec Coverage: X% (${thresholds.scopedSpecCoverage}% required)
│ └─ [list features WITH specs vs WITHOUT]
├─ Implementation Differential:
│ ├─ ✓ DONE: [criteria already implemented]
│ ├─ ◐ PARTIAL: [criteria partially done]
│ └─ ✗ REMAINING: [criteria to implement]
├─ Test Coverage: X% (${thresholds.scopedTestCoverage}% required)
│ └─ [files with tests vs without]
└─ QA Scores: [scores for files in scope]
\`\`\`
### PHASE 3: RECOMMENDATIONS (Scoped)
Based on scoped analysis, determine blockers:
**P0 BLOCKER** (must fix first):
- Scoped spec coverage < ${thresholds.scopedSpecCoverage}% → Create specs for uncovered features
- Critical dependencies missing specs → Spec those first
**P1 IMPORTANT** (address during task):
- Implementation differential shows gaps → Close gaps in order
- Test coverage < ${thresholds.scopedTestCoverage}% → Add tests for modified files
**P2 ENHANCEMENT** (note for later):
- QA scores below ${thresholds.scopedQaScore} → Improve after core work done
### PHASE 4: EXECUTE (${workflowType})
${requireSpecs ? '1. **SPEC** - Create/update specs for uncovered scope' : '1. Skip spec'}
2. **IMPLEMENT** - Work through implementation differential in order
3. **TEST** - Add/update tests for modified files
4. **COMMIT** - ${commitPerTask ? 'Semantic commit per logical unit' : 'Batch commits'}
5. **QA** - Verify scoped files meet qa≥${minQaScore}
### PHASE 5: VERIFY SCOPE
Before completing:
- [ ] All scoped features have specs (≥${thresholds.scopedSpecCoverage}%)
- [ ] Implementation differential closed (≥${thresholds.scopedImplComplete}%)
- [ ] Modified files have tests (≥${thresholds.scopedTestCoverage}%)
- [ ] QA scores meet threshold (≥${thresholds.scopedQaScore})
**MODE:** ${autonomyMode === 'full' ? 'AUTONOMOUS - Work until done, self-recover' : 'SUPERVISED'}
**Checkpoints:** Every ${checkpoint} actions, verify scope progress`
console.log(context)
}
main()