@allenpan2026/harshjudge 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +11 -0
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/cli.js +1869 -0
- package/dist/cli.js.map +1 -0
- package/dist/dashboard-worker.js +896 -0
- package/dist/dashboard-worker.js.map +1 -0
- package/package.json +64 -0
- package/skills/harshjudge/SKILL.md +152 -0
- package/skills/harshjudge/assets/prd.md +36 -0
- package/skills/harshjudge/references/create.md +258 -0
- package/skills/harshjudge/references/iterate.md +152 -0
- package/skills/harshjudge/references/run-playwright.md +41 -0
- package/skills/harshjudge/references/run-step-agent.md +65 -0
- package/skills/harshjudge/references/run.md +129 -0
- package/skills/harshjudge/references/setup.md +129 -0
- package/skills/harshjudge/references/status.md +134 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/ux/server/DashboardServer.ts","../src/types/scenario.ts","../src/types/run.ts","../src/schemas/index.ts","../src/ux/server/PathResolver.ts","../src/services/dashboard-worker.ts"],"sourcesContent":["import { createServer, IncomingMessage, ServerResponse, Server } from 'http';\nimport { readFile, stat, readdir } from 'fs/promises';\nimport { join, extname, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport type { AddressInfo } from 'net';\nimport yaml from 'js-yaml';\nimport type {\n HarshJudgeConfig,\n ScenarioStats,\n RunResult,\n ScenarioSummary,\n ScenarioDetail,\n RunSummary,\n StepInfo,\n} from '../../types/index.js';\nimport { DEFAULT_SCENARIO_STATS } from '../../types/index.js';\nimport { PathResolver, createPathResolver } from './PathResolver.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Extended ScenarioMeta that may include v2 fields\n */\ninterface ScenarioMetaV2 extends ScenarioStats {\n starred?: boolean;\n title?: string;\n tags?: string[];\n steps?: Array<{ id: string; title: string; file: string }>;\n}\n\n/**\n * Project summary for API response\n */\ninterface ServerProjectSummary {\n path: string;\n name: string;\n scenarioCount: number;\n overallStatus: 'pass' | 'fail' | 'never_run';\n}\n\n/**\n * Run detail for API response\n */\ninterface ServerRunDetail {\n runId: string;\n scenarioSlug: string;\n result: RunResult | null;\n evidencePaths: string[];\n}\n\n/**\n * Dashboard server configuration options\n */\nexport interface DashboardServerOptions {\n /** Port to listen on (default: 3000) */\n port?: number;\n /** Path to the project directory */\n projectPath?: string;\n /** Path to the dist directory with built assets */\n distPath?: string;\n}\n\n/**\n * Simple HTTP server for serving the HarshJudge dashboard\n * Features:\n * - Static file serving from dist/\n * - SPA routing (fallback to index.html)\n * - Graceful shutdown\n */\nexport class DashboardServer {\n private server: Server | null = null;\n private port: number;\n private pathResolver: PathResolver;\n private distPath: string;\n\n constructor(options: DashboardServerOptions = {}) {\n this.port = options.port ?? 3000;\n // Use createPathResolver to auto-handle if .harshJudge path was passed\n const projectPath = options.projectPath ?? process.cwd();\n this.pathResolver = createPathResolver(projectPath);\n // Default to the dist directory relative to this file's location\n this.distPath = options.distPath ?? join(__dirname, '../../dist');\n }\n\n /**\n * Start the dashboard server\n * @returns The actual port the server is listening on\n */\n async start(): Promise<number> {\n return new Promise((resolve, reject) => {\n this.server = createServer(this.handleRequest.bind(this));\n\n this.server.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n reject(new Error(`Port ${this.port} is already in use`));\n } else {\n reject(err);\n }\n });\n\n this.server.listen(this.port, () => {\n const address = this.server?.address() as AddressInfo;\n resolve(address.port);\n });\n });\n }\n\n /**\n * Stop the dashboard server\n */\n async stop(): Promise<void> {\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n /**\n * Check if the server is running\n */\n isRunning(): boolean {\n return this.server !== null && this.server.listening;\n }\n\n /**\n * Get the server URL\n */\n getUrl(): string {\n return `http://localhost:${this.port}`;\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private async handleRequest(\n req: IncomingMessage,\n res: ServerResponse\n ): Promise<void> {\n const url = req.url || '/';\n\n // Set CORS headers for development\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n // Only allow GET requests\n if (req.method !== 'GET') {\n res.writeHead(405);\n res.end('Method Not Allowed');\n return;\n }\n\n // Handle API routes\n if (url.startsWith('/api/')) {\n await this.handleApiRequest(url, res);\n return;\n }\n\n // Serve static files\n await this.serveStaticFile(url, res);\n }\n\n /**\n * Handle API requests\n */\n private async handleApiRequest(\n url: string,\n res: ServerResponse\n ): Promise<void> {\n // Parse URL and extract query string\n const urlParts = url.split('?');\n const cleanUrl = urlParts[0] ?? '';\n const queryString = urlParts[1] ?? '';\n const parts = cleanUrl\n .replace('/api/', '')\n .split('/')\n .map(decodeURIComponent);\n\n try {\n // GET /api/file?path=<absolute-path> - Serve evidence files\n if (parts[0] === 'file') {\n const params = new URLSearchParams(queryString);\n const filePath = params.get('path');\n\n if (!filePath) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Missing path parameter' }));\n return;\n }\n\n // Security: only allow serving files from .harshJudge directories\n if (!filePath.includes('.harshJudge')) {\n res.writeHead(403, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n error: 'Access denied: can only serve HarshJudge evidence files',\n })\n );\n return;\n }\n\n // Security: prevent directory traversal\n if (filePath.includes('..')) {\n res.writeHead(403, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n error: 'Access denied: path traversal not allowed',\n })\n );\n return;\n }\n\n try {\n const content = await readFile(filePath);\n const contentType = this.getContentType(filePath);\n\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Cache-Control': 'public, max-age=3600',\n });\n res.end(content);\n return;\n } catch {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'File not found' }));\n return;\n }\n }\n\n // GET /api/projects\n if (parts[0] === 'projects' && parts.length === 1) {\n const projects = await this.getProjects();\n this.sendJson(res, projects);\n return;\n }\n\n // GET /api/projects/:projectPath/scenarios\n if (\n parts[0] === 'projects' &&\n parts[2] === 'scenarios' &&\n parts.length === 3\n ) {\n const projectPath = parts[1];\n const scenarios = await this.getScenarios(projectPath ?? '');\n this.sendJson(res, scenarios);\n return;\n }\n\n // GET /api/projects/:projectPath/scenarios/:scenarioSlug\n if (\n parts[0] === 'projects' &&\n parts[2] === 'scenarios' &&\n parts.length === 4\n ) {\n const projectPath = parts[1];\n const scenarioSlug = parts[3];\n const detail = await this.getScenarioDetail(\n projectPath ?? '',\n scenarioSlug ?? ''\n );\n this.sendJson(res, detail);\n return;\n }\n\n // GET /api/projects/:projectPath/scenarios/:scenarioSlug/runs\n if (\n parts[0] === 'projects' &&\n parts[2] === 'scenarios' &&\n parts[4] === 'runs' &&\n parts.length === 5\n ) {\n const projectPath = parts[1];\n const scenarioSlug = parts[3];\n const runs = await this.getRunHistory(\n projectPath ?? '',\n scenarioSlug ?? ''\n );\n this.sendJson(res, runs);\n return;\n }\n\n // GET /api/projects/:projectPath/scenarios/:scenarioSlug/runs/:runId\n if (\n parts[0] === 'projects' &&\n parts[2] === 'scenarios' &&\n parts[4] === 'runs' &&\n parts.length === 6\n ) {\n const projectPath = parts[1];\n const scenarioSlug = parts[3];\n const runId = parts[5];\n const detail = await this.getRunDetail(\n projectPath ?? '',\n scenarioSlug ?? '',\n runId ?? ''\n );\n this.sendJson(res, detail);\n return;\n }\n\n // Not found\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'API endpoint not found' }));\n } catch (err) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n error: err instanceof Error ? err.message : 'Internal server error',\n })\n );\n }\n }\n\n /**\n * Send JSON response\n */\n private sendJson(res: ServerResponse, data: unknown): void {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(data));\n }\n\n /**\n * Serve a static file or fallback to index.html for SPA routing\n */\n private async serveStaticFile(\n url: string,\n res: ServerResponse\n ): Promise<void> {\n // Clean URL (remove query string, handle trailing slash)\n let cleanUrl = url.split('?')[0];\n if (cleanUrl === '/') {\n cleanUrl = '/index.html';\n }\n\n // Decode URL to catch encoded traversal attempts like %2e%2e\n const decodedUrl = decodeURIComponent(cleanUrl ?? '');\n\n // Security: prevent directory traversal (check both encoded and decoded)\n if ((cleanUrl ?? '').includes('..') || decodedUrl.includes('..')) {\n res.writeHead(403);\n res.end('Forbidden');\n return;\n }\n\n let filePath = join(this.distPath, decodedUrl);\n\n try {\n const stats = await stat(filePath);\n\n if (stats.isDirectory()) {\n filePath = join(filePath, 'index.html');\n }\n\n const content = await readFile(filePath);\n const contentType = this.getContentType(filePath);\n\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Cache-Control': 'no-cache',\n });\n res.end(content);\n } catch {\n // SPA fallback - serve index.html for non-file routes\n try {\n const indexPath = join(this.distPath, 'index.html');\n const indexContent = await readFile(indexPath);\n\n res.writeHead(200, {\n 'Content-Type': 'text/html',\n 'Cache-Control': 'no-cache',\n });\n res.end(indexContent);\n } catch {\n res.writeHead(404);\n res.end('Not Found - Dashboard not built. Run `npm run build` first.');\n }\n }\n }\n\n /**\n * Get MIME type for a file based on its extension\n */\n private getContentType(filePath: string): string {\n const ext = extname(filePath).toLowerCase();\n const mimeTypes: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'application/javascript; charset=utf-8',\n '.mjs': 'application/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.eot': 'application/vnd.ms-fontobject',\n };\n\n return mimeTypes[ext] || 'application/octet-stream';\n }\n\n // --- Data Service Methods ---\n\n /**\n * Discover all HarshJudge projects in the project path\n */\n private async getProjects(): Promise<ServerProjectSummary[]> {\n try {\n const harshJudgePath = this.pathResolver.getHarshJudgePath();\n const exists = await this.pathExists(harshJudgePath);\n\n if (!exists) {\n return [];\n }\n\n const config = await this.readConfig();\n if (!config) {\n return [];\n }\n\n // Use project root for getScenarios (it will use pathResolver internally)\n const scenarios = await this.getScenarios(\n this.pathResolver.getProjectRoot()\n );\n const overallStatus = this.calculateOverallStatus(scenarios);\n\n return [\n {\n path: this.pathResolver.getProjectRoot(),\n name: config.projectName,\n scenarioCount: scenarios.length,\n overallStatus,\n },\n ];\n } catch {\n return [];\n }\n }\n\n /**\n * Get all scenarios for a project\n * @param projectPath - Project root path (NOT the .harshJudge path)\n */\n private async getScenarios(projectPath: string): Promise<ScenarioSummary[]> {\n try {\n // Create a resolver for the given project path\n const resolver = createPathResolver(projectPath);\n const scenariosPath = resolver.getScenariosDir();\n const exists = await this.pathExists(scenariosPath);\n\n if (!exists) {\n return [];\n }\n\n const entries = await readdir(scenariosPath, { withFileTypes: true });\n const scenarioDirs = entries.filter((e) => e.isDirectory());\n\n const scenarios: ScenarioSummary[] = [];\n\n for (const dir of scenarioDirs) {\n const scenario = await this.readScenarioSummary(\n resolver.getScenarioDir(dir.name),\n dir.name\n );\n if (scenario) {\n scenarios.push(scenario);\n }\n }\n\n // Sort by last run time (most recent first)\n return scenarios.sort((a, b) => {\n if (!a.lastRun && !b.lastRun) return 0;\n if (!a.lastRun) return 1;\n if (!b.lastRun) return -1;\n return new Date(b.lastRun).getTime() - new Date(a.lastRun).getTime();\n });\n } catch {\n return [];\n }\n }\n\n /**\n * Get detailed scenario information including runs\n * @param projectPath - Project root path (NOT the .harshJudge path)\n */\n private async getScenarioDetail(\n projectPath: string,\n slug: string\n ): Promise<ScenarioDetail | null> {\n try {\n const resolver = createPathResolver(projectPath);\n const scenarioPath = resolver.getScenarioDir(slug);\n const exists = await this.pathExists(scenarioPath);\n\n if (!exists) {\n return null;\n }\n\n const [scenarioContent, meta] = await Promise.all([\n this.readScenarioContent(scenarioPath),\n this.readScenarioMeta(scenarioPath),\n ]);\n\n // v2 structure: meta.yaml has title, scenario.md is optional\n // v1 structure: scenario.md is required\n if (!meta && !scenarioContent) {\n return null;\n }\n\n const recentRuns = await this.getRecentRuns(scenarioPath, 10);\n const metaData = meta || this.defaultMeta();\n\n // Extract steps from meta.yaml (v2 structure)\n const steps: StepInfo[] = (meta?.steps || []).map((step) => ({\n id: step.id,\n title: step.title,\n }));\n\n return {\n slug,\n title: meta?.title || scenarioContent?.title || slug,\n starred: meta?.starred ?? false,\n tags: meta?.tags || scenarioContent?.tags || [],\n stepCount: steps.length,\n steps,\n content: scenarioContent?.content || '',\n meta: metaData,\n recentRuns,\n };\n } catch {\n return null;\n }\n }\n\n /**\n * Get run history for a scenario\n * @param projectPath - Project root path (NOT the .harshJudge path)\n */\n private async getRunHistory(\n projectPath: string,\n scenarioSlug: string\n ): Promise<RunSummary[]> {\n try {\n const resolver = createPathResolver(projectPath);\n const scenarioPath = resolver.getScenarioDir(scenarioSlug);\n return await this.getRecentRuns(scenarioPath, 100);\n } catch {\n return [];\n }\n }\n\n /**\n * Get run detail including evidence paths\n * @param projectPath - Project root path (NOT the .harshJudge path)\n */\n private async getRunDetail(\n projectPath: string,\n scenarioSlug: string,\n runId: string\n ): Promise<ServerRunDetail | null> {\n try {\n const resolver = createPathResolver(projectPath);\n const runPath = resolver.getRunDir(scenarioSlug, runId);\n const exists = await this.pathExists(runPath);\n\n if (!exists) {\n return null;\n }\n\n const result = await this.readRunResult(runPath);\n const evidencePaths = await this.getEvidencePaths(runPath);\n\n return {\n runId,\n scenarioSlug,\n result,\n evidencePaths,\n };\n } catch {\n return null;\n }\n }\n\n // --- Private helper methods ---\n\n private async pathExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n }\n\n private async readConfig(): Promise<HarshJudgeConfig | null> {\n try {\n const configPath = this.pathResolver.getConfigPath();\n const content = await readFile(configPath, 'utf-8');\n return yaml.load(content) as HarshJudgeConfig;\n } catch {\n return null;\n }\n }\n\n private async readScenarioMeta(\n scenarioPath: string\n ): Promise<ScenarioMetaV2 | null> {\n try {\n const metaPath = join(scenarioPath, 'meta.yaml');\n const content = await readFile(metaPath, 'utf-8');\n return yaml.load(content) as ScenarioMetaV2;\n } catch {\n return null;\n }\n }\n\n private async readScenarioContent(\n scenarioPath: string\n ): Promise<{ title: string; tags: string[]; content: string } | null> {\n try {\n const scenarioFile = join(scenarioPath, 'scenario.md');\n const content = await readFile(scenarioFile, 'utf-8');\n\n // Parse YAML frontmatter\n const frontmatterMatch = content.match(\n /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/\n );\n if (\n !frontmatterMatch ||\n !frontmatterMatch[1] ||\n frontmatterMatch[2] === undefined\n ) {\n return null;\n }\n\n const frontmatter = yaml.load(frontmatterMatch[1]) as {\n title: string;\n tags: string[];\n };\n\n return {\n title: frontmatter.title || 'Untitled',\n tags: frontmatter.tags || [],\n content: frontmatterMatch[2],\n };\n } catch {\n return null;\n }\n }\n\n private async readScenarioSummary(\n scenarioPath: string,\n slug: string\n ): Promise<ScenarioSummary | null> {\n const [meta, scenario] = await Promise.all([\n this.readScenarioMeta(scenarioPath),\n this.readScenarioContent(scenarioPath),\n ]);\n\n // v2 structure: meta.yaml has title, scenario.md is optional\n // v1 structure: scenario.md is required\n if (!meta && !scenario) {\n return null;\n }\n\n const metaData = meta || this.defaultMeta();\n const passRate =\n metaData.totalRuns > 0\n ? (metaData.passCount / metaData.totalRuns) * 100\n : 0;\n\n return {\n slug,\n title: meta?.title || scenario?.title || slug,\n starred: meta?.starred ?? false,\n tags: meta?.tags || scenario?.tags || [],\n stepCount: meta?.steps?.length ?? 0,\n lastResult: metaData.lastResult,\n lastRun: metaData.lastRun,\n totalRuns: metaData.totalRuns,\n passRate,\n };\n }\n\n private async readRunResult(runPath: string): Promise<RunResult | null> {\n try {\n const resultPath = join(runPath, 'result.json');\n const content = await readFile(resultPath, 'utf-8');\n return JSON.parse(content) as RunResult;\n } catch {\n return null;\n }\n }\n\n private async getRecentRuns(\n scenarioPath: string,\n limit: number\n ): Promise<RunSummary[]> {\n try {\n const runsPath = join(scenarioPath, 'runs');\n const exists = await this.pathExists(runsPath);\n\n if (!exists) {\n return [];\n }\n\n const entries = await readdir(runsPath, { withFileTypes: true });\n const runDirs = entries.filter((e) => e.isDirectory());\n\n const runs: RunSummary[] = [];\n\n for (const dir of runDirs) {\n const runPath = join(runsPath, dir.name);\n const result = await this.readRunResult(runPath);\n\n if (result) {\n // Support both completed runs (pass/fail) and running runs\n const status = result.status as 'pass' | 'fail' | 'running';\n runs.push({\n id: result.runId,\n runNumber: runs.length + 1,\n status,\n duration: result.duration ?? 0,\n startedAt: result.startedAt,\n completedAt: result.completedAt,\n errorMessage: result.errorMessage ?? null,\n });\n }\n }\n\n // Sort by startedAt (most recent first), fallback to completedAt, and limit\n return runs\n .sort((a, b) => {\n const aTime = new Date(a.startedAt || a.completedAt || 0).getTime();\n const bTime = new Date(b.startedAt || b.completedAt || 0).getTime();\n return bTime - aTime;\n })\n .slice(0, limit);\n } catch {\n return [];\n }\n }\n\n private async getEvidencePaths(runPath: string): Promise<string[]> {\n try {\n const paths: string[] = [];\n\n // Check for v2 structure (per-step evidence directories)\n const runEntries = await readdir(runPath, { withFileTypes: true });\n const stepDirs = runEntries.filter(\n (e) => e.isDirectory() && e.name.startsWith('step-')\n );\n\n if (stepDirs.length > 0) {\n // v2 structure: read evidence from each step directory\n for (const stepDir of stepDirs) {\n const stepEvidencePath = join(runPath, stepDir.name, 'evidence');\n if (await this.pathExists(stepEvidencePath)) {\n const entries = await readdir(stepEvidencePath);\n for (const entry of entries) {\n if (!entry.endsWith('.meta.json')) {\n paths.push(join(stepEvidencePath, entry));\n }\n }\n }\n }\n return paths;\n }\n\n // v1 structure: flat evidence directory at run root\n const evidencePath = join(runPath, 'evidence');\n const exists = await this.pathExists(evidencePath);\n\n if (!exists) {\n return [];\n }\n\n const entries = await readdir(evidencePath);\n // Filter out .meta.json files, return only actual evidence files\n return entries\n .filter((e) => !e.endsWith('.meta.json'))\n .map((e) => join(evidencePath, e));\n } catch {\n return [];\n }\n }\n\n private calculateOverallStatus(\n scenarios: ScenarioSummary[]\n ): 'pass' | 'fail' | 'never_run' {\n if (scenarios.length === 0) {\n return 'never_run';\n }\n\n const hasFailure = scenarios.some((s) => s.lastResult === 'fail');\n if (hasFailure) {\n return 'fail';\n }\n\n const hasPass = scenarios.some((s) => s.lastResult === 'pass');\n if (hasPass) {\n return 'pass';\n }\n\n return 'never_run';\n }\n\n private defaultMeta(): ScenarioStats {\n return { ...DEFAULT_SCENARIO_STATS };\n }\n}\n","import { z } from 'zod';\n\n// ============================================================\n// Step Types (NEW in v2)\n// ============================================================\n\n/**\n * Reference to a step file in meta.yaml\n */\nexport const StepReferenceSchema = z.object({\n id: z.string().regex(/^\\d{2}$/, 'Step ID must be zero-padded (01, 02, etc.)'),\n title: z.string().min(1),\n file: z.string().regex(/^\\d{2}-[\\w-]+\\.md$/, 'Step file must match pattern: {id}-{slug}.md'),\n});\nexport type StepReference = z.infer<typeof StepReferenceSchema>;\n\n/**\n * Full step definition with content sections\n */\nexport interface Step {\n id: string;\n title: string;\n description: string;\n preconditions: string;\n actions: string;\n expectedOutcome: string;\n}\n\n/**\n * Step file read from filesystem\n */\nexport interface StepFile {\n id: string;\n title: string;\n content: string; // Full markdown content\n}\n\n// ============================================================\n// Scenario Types (v2)\n// ============================================================\n\n/**\n * Test scenario definition (v2 - with steps)\n */\nexport interface Scenario {\n slug: string;\n title: string;\n starred: boolean;\n tags: string[];\n estimatedDuration: number;\n steps: StepReference[];\n}\n\n/**\n * Full scenario metadata stored in `meta.yaml` (v2)\n * Combines scenario definition + machine-updated statistics\n */\nexport const ScenarioMetaSchema = z.object({\n // Scenario definition\n slug: z.string().regex(/^[a-z0-9-]+$/),\n title: z.string().min(1),\n starred: z.boolean().default(false),\n tags: z.array(z.string()).default([]),\n estimatedDuration: z.number().positive().default(60),\n steps: z.array(StepReferenceSchema).default([]),\n\n // Statistics (machine-updated)\n totalRuns: z.number().nonnegative().default(0),\n passCount: z.number().nonnegative().default(0),\n failCount: z.number().nonnegative().default(0),\n lastRun: z.string().nullable().default(null),\n lastResult: z.enum(['pass', 'fail']).nullable().default(null),\n avgDuration: z.number().nonnegative().default(0),\n});\nexport type ScenarioMeta = z.infer<typeof ScenarioMetaSchema>;\n\n// ============================================================\n// Statistics Types (shared between v1 and v2)\n// ============================================================\n\n/**\n * Machine-updated statistics (shared structure)\n */\nexport interface ScenarioStats {\n totalRuns: number;\n passCount: number;\n failCount: number;\n lastRun: string | null;\n lastResult: 'pass' | 'fail' | null;\n avgDuration: number;\n}\n\n/**\n * Default statistics values for new scenarios\n */\nexport const DEFAULT_SCENARIO_STATS: ScenarioStats = {\n totalRuns: 0,\n passCount: 0,\n failCount: 0,\n lastRun: null,\n lastResult: null,\n avgDuration: 0,\n};\n\n// ============================================================\n// Legacy Types (v1 - deprecated)\n// ============================================================\n\n/**\n * @deprecated Use Scenario instead (v2)\n * Test scenario definition stored as `scenario.md` with YAML frontmatter\n */\nexport interface ScenarioV1 {\n id: string;\n title: string;\n tags: string[];\n estimatedDuration: number;\n content: string;\n}\n\n/**\n * @deprecated Use ScenarioMeta instead (v2)\n * YAML frontmatter extracted from scenario.md\n */\nexport interface ScenarioFrontmatter {\n id: string;\n title: string;\n tags: string[];\n estimatedDuration: number;\n}\n\n/**\n * @deprecated Use ScenarioStats instead\n * Machine-updated statistics stored in `meta.yaml`\n */\nexport type ScenarioMetaV1 = ScenarioStats;\n\n// ============================================================\n// Utility Functions\n// ============================================================\n\n/**\n * Generate zero-padded step ID from number\n */\nexport function padStepId(n: number): string {\n return String(n).padStart(2, '0');\n}\n\n/**\n * Generate step filename from id and title\n */\nexport function generateStepFilename(id: string, title: string): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n return `${id}-${slug}.md`;\n}\n","import { z } from 'zod';\n\n/**\n * Result of a single step execution\n */\nexport const StepResultSchema = z.object({\n id: z.string().regex(/^\\d{2}$/, 'Step ID must be zero-padded'),\n status: z.enum(['pass', 'fail', 'skipped']),\n duration: z.number().nonnegative().optional().default(0),\n error: z.string().nullable().default(null),\n evidenceFiles: z.array(z.string()).default([]),\n /** AI-generated summary describing what happened in this step */\n summary: z.string().nullable().optional().default(null),\n});\nexport type StepResult = z.infer<typeof StepResultSchema>;\n\n/**\n * Single execution of a scenario\n */\nexport interface Run {\n id: string;\n scenarioSlug: string;\n runNumber: number;\n startedAt: string;\n status: 'running' | 'completed';\n}\n\n/**\n * Final outcome of a run stored in `result.json` (v2)\n */\nexport const RunResultSchema = z.object({\n runId: z.string(),\n scenarioSlug: z.string().optional(), // Optional for backward compat\n status: z.enum(['pass', 'fail', 'running']),\n startedAt: z.string(),\n completedAt: z.string().optional(),\n duration: z.number().nonnegative().optional().default(0),\n steps: z.array(StepResultSchema).default([]),\n failedStep: z.string().nullable().default(null), // Changed from number to string (step ID)\n errorMessage: z.string().nullable().default(null),\n});\nexport type RunResult = z.infer<typeof RunResultSchema>;\n\n/**\n * @deprecated Legacy v1 result format\n */\nexport interface RunResultV1 {\n runId: string;\n status: 'pass' | 'fail';\n duration: number;\n completedAt: string;\n failedStep: number | null;\n errorMessage: string | null;\n stepCount: number;\n evidenceCount: number;\n}\n","import { z } from 'zod';\nimport type { ProjectStatus, ScenarioDetail } from '../types/status.js';\nimport { StepResultSchema } from '../types/run.js';\n\n// ============================================================\n// initProject\n// ============================================================\n\nexport const InitProjectParamsSchema = z.object({\n projectName: z.string().min(1).max(100),\n baseUrl: z.string().url().optional(),\n});\nexport type InitProjectParams = z.infer<typeof InitProjectParamsSchema>;\n\nexport interface InitProjectResult {\n success: boolean;\n projectPath: string;\n configPath: string;\n prdPath: string;\n scenariosPath: string;\n dashboardUrl?: string;\n message?: string;\n}\n\n// ============================================================\n// saveScenario (DEPRECATED - use createScenario instead)\n// ============================================================\n\n/** @deprecated Use CreateScenarioParamsSchema instead */\nexport const SaveScenarioParamsSchema = z.object({\n slug: z\n .string()\n .regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'),\n title: z.string().min(1).max(200),\n content: z.string().min(1),\n tags: z.array(z.string()).optional().default([]),\n estimatedDuration: z.number().positive().optional().default(60),\n});\n/** @deprecated Use CreateScenarioParams instead */\nexport type SaveScenarioParams = z.infer<typeof SaveScenarioParamsSchema>;\n\n/** @deprecated Use CreateScenarioResult instead */\nexport interface SaveScenarioResult {\n success: boolean;\n slug: string;\n scenarioPath: string;\n metaPath: string;\n isNew: boolean;\n}\n\n// ============================================================\n// createScenario (NEW - replaces saveScenario)\n// ============================================================\n\nexport const StepInputSchema = z.object({\n title: z.string().min(1),\n description: z.string().optional().default(''),\n preconditions: z.string().optional().default(''),\n actions: z.string().min(1),\n expectedOutcome: z.string().min(1),\n});\nexport type StepInput = z.infer<typeof StepInputSchema>;\n\nexport const CreateScenarioParamsSchema = z.object({\n slug: z\n .string()\n .regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'),\n title: z.string().min(1).max(200),\n steps: z.array(StepInputSchema).min(1),\n tags: z.array(z.string()).optional().default([]),\n estimatedDuration: z.number().positive().optional().default(60),\n starred: z.boolean().optional().default(false),\n});\nexport type CreateScenarioParams = z.infer<typeof CreateScenarioParamsSchema>;\n\nexport interface CreateScenarioResult {\n success: boolean;\n slug: string;\n scenarioPath: string;\n metaPath: string;\n stepsPath: string;\n stepFiles: string[];\n isNew: boolean;\n}\n\n// ============================================================\n// toggleStar\n// ============================================================\n\nexport const ToggleStarParamsSchema = z.object({\n scenarioSlug: z.string().regex(/^[a-z0-9-]+$/),\n starred: z.boolean().optional(), // If omitted, toggles current state\n});\nexport type ToggleStarParams = z.infer<typeof ToggleStarParamsSchema>;\n\nexport interface ToggleStarResult {\n success: boolean;\n slug: string;\n starred: boolean;\n}\n\n// ============================================================\n// startRun\n// ============================================================\n\nexport const StartRunParamsSchema = z.object({\n scenarioSlug: z.string().regex(/^[a-z0-9-]+$/),\n});\nexport type StartRunParams = z.infer<typeof StartRunParamsSchema>;\n\nexport interface StartRunStepInfo {\n id: string; // Zero-padded step ID (e.g., \"01\", \"02\")\n title: string;\n file: string; // Filename in steps/ directory\n}\n\nexport interface StartRunResult {\n success: boolean;\n runId: string;\n runNumber: number;\n runPath: string;\n evidencePath: string;\n startedAt: string;\n // NEW: Step information for orchestration\n scenarioSlug: string;\n scenarioTitle: string;\n steps: StartRunStepInfo[];\n}\n\n// ============================================================\n// recordEvidence (v2 - stepId instead of step number)\n// ============================================================\n\nexport const RecordEvidenceParamsSchema = z.object({\n runId: z.string().min(1),\n step: z.number().int().positive(), // v2: accepts number, will be converted to zero-padded string\n type: z.enum([\n 'screenshot',\n 'db_snapshot',\n 'console_log',\n 'network_log',\n 'html_snapshot',\n 'custom',\n ]),\n name: z.string().min(1).max(100),\n data: z.string(), // For screenshot: absolute file path; for others: content\n metadata: z.record(z.unknown()).optional(),\n});\nexport type RecordEvidenceParams = z.infer<typeof RecordEvidenceParamsSchema>;\n\nexport interface RecordEvidenceResult {\n success: boolean;\n filePath: string;\n metaPath: string;\n stepPath: string; // NEW: path to the step's evidence directory\n fileSize: number;\n}\n\n// ============================================================\n// completeRun (v2 - step ID string, optional steps array)\n// ============================================================\n\nexport const CompleteRunParamsSchema = z.object({\n runId: z.string().min(1),\n status: z.enum(['pass', 'fail']),\n duration: z.number().nonnegative(),\n failedStep: z\n .string()\n .regex(/^\\d{2}$/, 'Step ID must be zero-padded')\n .optional(), // Changed from number to string\n errorMessage: z.string().optional(),\n steps: z.array(StepResultSchema).optional(), // NEW: per-step results\n});\nexport type CompleteRunParams = z.infer<typeof CompleteRunParamsSchema>;\n\nexport interface CompleteRunResult {\n success: boolean;\n resultPath: string;\n updatedMeta: {\n totalRuns: number;\n passCount: number;\n failCount: number;\n avgDuration: number;\n };\n}\n\n// ============================================================\n// completeStep (NEW in v2)\n// ============================================================\n\nexport const CompleteStepParamsSchema = z.object({\n runId: z.string().min(1),\n stepId: z\n .string()\n .regex(/^\\d{2}$/, 'Step ID must be zero-padded (e.g., \"01\", \"02\")'),\n status: z.enum(['pass', 'fail', 'skipped']),\n duration: z.number().nonnegative(),\n error: z.string().optional(),\n /** AI-generated summary describing what happened in this step and match result */\n summary: z.string().optional(),\n});\nexport type CompleteStepParams = z.infer<typeof CompleteStepParamsSchema>;\n\nexport interface CompleteStepResult {\n success: boolean;\n runId: string;\n stepId: string;\n status: 'pass' | 'fail' | 'skipped';\n nextStepId: string | null; // null if last step or should stop\n}\n\n// ============================================================\n// getStatus\n// ============================================================\n\nexport const GetStatusParamsSchema = z.object({\n scenarioSlug: z\n .string()\n .regex(/^[a-z0-9-]+$/)\n .optional(),\n starredOnly: z.boolean().optional().default(false),\n});\nexport type GetStatusParams = z.infer<typeof GetStatusParamsSchema>;\n\nexport type GetStatusResult = ProjectStatus | ScenarioDetail;\n\n// ============================================================\n// openDashboard\n// ============================================================\n\nexport const OpenDashboardParamsSchema = z.object({\n port: z.number().int().min(1024).max(65535).optional(),\n openBrowser: z.boolean().optional().default(true),\n projectPath: z\n .string()\n .optional()\n .describe(\n 'Path to the project directory containing .harshJudge folder. Defaults to current working directory.'\n ),\n});\nexport type OpenDashboardParams = z.infer<typeof OpenDashboardParamsSchema>;\n\nexport interface OpenDashboardResult {\n success: boolean;\n url: string;\n port: number;\n pid: number;\n alreadyRunning: boolean;\n message: string;\n}\n\n// ============================================================\n// closeDashboard\n// ============================================================\n\nexport const CloseDashboardParamsSchema = z.object({\n projectPath: z\n .string()\n .optional()\n .describe(\n 'Path to the project directory containing .harshJudge folder. Defaults to current working directory.'\n ),\n});\nexport type CloseDashboardParams = z.infer<typeof CloseDashboardParamsSchema>;\n\nexport interface CloseDashboardResult {\n success: boolean;\n wasRunning: boolean;\n message: string;\n}\n\n// ============================================================\n// getDashboardStatus\n// ============================================================\n\nexport const GetDashboardStatusParamsSchema = z.object({\n projectPath: z\n .string()\n .optional()\n .describe(\n 'Path to the project directory containing .harshJudge folder. Defaults to current working directory.'\n ),\n});\nexport type GetDashboardStatusParams = z.infer<\n typeof GetDashboardStatusParamsSchema\n>;\n\nexport interface GetDashboardStatusResult {\n running: boolean;\n pid?: number;\n port?: number;\n url?: string;\n startedAt?: string;\n stale?: boolean;\n message: string;\n}\n","import { join } from 'path';\n\n/**\n * HarshJudge directory structure:\n *\n * {projectRoot}/\n * └── .harshJudge/\n * ├── config.yaml\n * ├── prd.md\n * └── scenarios/\n * └── {scenarioSlug}/\n * ├── meta.yaml\n * ├── steps/\n * │ └── {stepId}-{stepTitle}.md\n * └── runs/\n * └── {runId}/\n * ├── result.json\n * └── step-{stepId}/\n * └── evidence/\n * └── {evidenceName}.*\n */\n\nconst HARSHJUDGE_DIR = '.harshJudge';\n\n/**\n * PathResolver provides centralized, template-based path generation\n * for all HarshJudge file operations.\n *\n * All paths are derived from a validated project root path.\n */\nexport class PathResolver {\n private readonly projectRoot: string;\n private readonly harshJudgePath: string;\n\n /**\n * Create a PathResolver from a project root path.\n * @param projectRoot - The project root directory (must NOT contain .harshJudge)\n * @throws Error if projectRoot contains .harshJudge\n */\n constructor(projectRoot: string) {\n // Validate: projectRoot must not contain .harshJudge\n if (projectRoot.includes(HARSHJUDGE_DIR)) {\n throw new Error(\n `Invalid projectRoot: \"${projectRoot}\" contains \"${HARSHJUDGE_DIR}\". ` +\n `Please provide the project working directory, not the .harshJudge path.`\n );\n }\n\n this.projectRoot = projectRoot;\n this.harshJudgePath = join(projectRoot, HARSHJUDGE_DIR);\n }\n\n // --- Root Paths ---\n\n /** Get the project root directory */\n getProjectRoot(): string {\n return this.projectRoot;\n }\n\n /** Get the .harshJudge directory path */\n getHarshJudgePath(): string {\n return this.harshJudgePath;\n }\n\n // --- Config Paths ---\n\n /** Get config.yaml path */\n getConfigPath(): string {\n return join(this.harshJudgePath, 'config.yaml');\n }\n\n /** Get prd.md path */\n getPrdPath(): string {\n return join(this.harshJudgePath, 'prd.md');\n }\n\n // --- Scenarios Paths ---\n\n /** Get the scenarios directory path */\n getScenariosDir(): string {\n return join(this.harshJudgePath, 'scenarios');\n }\n\n /** Get a specific scenario directory path */\n getScenarioDir(scenarioSlug: string): string {\n return join(this.getScenariosDir(), scenarioSlug);\n }\n\n /** Get scenario meta.yaml path */\n getScenarioMetaPath(scenarioSlug: string): string {\n return join(this.getScenarioDir(scenarioSlug), 'meta.yaml');\n }\n\n /** Get scenario.md path (v1 structure, optional) */\n getScenarioContentPath(scenarioSlug: string): string {\n return join(this.getScenarioDir(scenarioSlug), 'scenario.md');\n }\n\n // --- Steps Paths ---\n\n /** Get the steps directory for a scenario */\n getStepsDir(scenarioSlug: string): string {\n return join(this.getScenarioDir(scenarioSlug), 'steps');\n }\n\n /** Get a specific step file path */\n getStepPath(scenarioSlug: string, stepFileName: string): string {\n return join(this.getStepsDir(scenarioSlug), stepFileName);\n }\n\n // --- Runs Paths ---\n\n /** Get the runs directory for a scenario */\n getRunsDir(scenarioSlug: string): string {\n return join(this.getScenarioDir(scenarioSlug), 'runs');\n }\n\n /** Get a specific run directory path */\n getRunDir(scenarioSlug: string, runId: string): string {\n return join(this.getRunsDir(scenarioSlug), runId);\n }\n\n /** Get run result.json path */\n getRunResultPath(scenarioSlug: string, runId: string): string {\n return join(this.getRunDir(scenarioSlug, runId), 'result.json');\n }\n\n // --- Run Step Evidence Paths ---\n\n /** Get the step directory within a run */\n getRunStepDir(scenarioSlug: string, runId: string, stepId: string): string {\n return join(this.getRunDir(scenarioSlug, runId), `step-${stepId}`);\n }\n\n /** Get the evidence directory for a step within a run */\n getRunStepEvidenceDir(scenarioSlug: string, runId: string, stepId: string): string {\n return join(this.getRunStepDir(scenarioSlug, runId, stepId), 'evidence');\n }\n\n /** Get a specific evidence file path */\n getEvidencePath(scenarioSlug: string, runId: string, stepId: string, fileName: string): string {\n return join(this.getRunStepEvidenceDir(scenarioSlug, runId, stepId), fileName);\n }\n\n /** Get evidence metadata file path */\n getEvidenceMetaPath(scenarioSlug: string, runId: string, stepId: string, fileName: string): string {\n // Meta files are named {fileName}.meta.json\n return join(this.getRunStepEvidenceDir(scenarioSlug, runId, stepId), `${fileName}.meta.json`);\n }\n\n // --- Legacy v1 Evidence Paths (flat structure at run root) ---\n\n /** Get legacy flat evidence directory at run root */\n getLegacyEvidenceDir(scenarioSlug: string, runId: string): string {\n return join(this.getRunDir(scenarioSlug, runId), 'evidence');\n }\n\n // --- Utility Methods ---\n\n /**\n * Check if a path is within the .harshJudge directory (for security)\n */\n isWithinHarshJudge(path: string): boolean {\n return path.startsWith(this.harshJudgePath);\n }\n\n /**\n * Validate that a path is safe (within .harshJudge and no traversal)\n */\n isPathSafe(path: string): boolean {\n if (path.includes('..')) {\n return false;\n }\n return this.isWithinHarshJudge(path);\n }\n}\n\n/**\n * Create a PathResolver from any path, auto-detecting if it's already\n * a .harshJudge path and extracting the project root.\n *\n * @param path - Either a project root or a .harshJudge path\n * @returns PathResolver instance\n * @throws Error if path structure is invalid\n */\nexport function createPathResolver(path: string): PathResolver {\n // If path contains .harshJudge, extract the project root\n const harshJudgeIndex = path.indexOf(HARSHJUDGE_DIR);\n\n if (harshJudgeIndex !== -1) {\n // Extract everything before .harshJudge\n const projectRoot = path.substring(0, harshJudgeIndex).replace(/\\/$/, '');\n if (!projectRoot) {\n throw new Error(`Invalid path: cannot determine project root from \"${path}\"`);\n }\n return new PathResolver(projectRoot);\n }\n\n // Path is already a project root\n return new PathResolver(path);\n}\n","/**\n * Dashboard Worker Process\n *\n * This script is forked by DashboardManager to run the dashboard server\n * in a detached process. It reads configuration from environment variables\n * and starts the DashboardServer.\n *\n * Environment variables:\n * - HARSHJUDGE_PORT: Port to listen on (default: 7002)\n * - HARSHJUDGE_PROJECT_PATH: Path to the project directory\n */\n\n// Import from the actual file path since tsup doesn't resolve subpath exports\nimport { DashboardServer } from '../ux/server/DashboardServer.js';\nimport { dirname, join } from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nasync function main(): Promise<void> {\n const port = parseInt(process.env['HARSHJUDGE_PORT'] || '7002', 10);\n const projectPath = process.env['HARSHJUDGE_PROJECT_PATH'] || process.cwd();\n\n // The dist path for static assets - copied alongside this worker during build\n // __dirname is the dist folder where dashboard-worker.js resides\n // ux-dist is copied there by the tsup onSuccess hook\n const distPath = join(__dirname, 'ux-dist');\n\n console.log(`[HarshJudge Worker] Starting dashboard server...`);\n console.log(`[HarshJudge Worker] Port: ${port}`);\n console.log(`[HarshJudge Worker] Project path: ${projectPath}`);\n console.log(`[HarshJudge Worker] Dist path: ${distPath}`);\n\n const server = new DashboardServer({\n port,\n projectPath,\n distPath,\n });\n\n try {\n const actualPort = await server.start();\n console.log(\n `[HarshJudge Worker] Dashboard running at http://localhost:${actualPort}`\n );\n\n // Keep the process alive\n process.on('SIGTERM', async () => {\n console.log('[HarshJudge Worker] Received SIGTERM, shutting down...');\n await server.stop();\n process.exit(0);\n });\n\n process.on('SIGINT', async () => {\n console.log('[HarshJudge Worker] Received SIGINT, shutting down...');\n await server.stop();\n process.exit(0);\n });\n } catch (error) {\n console.error('[HarshJudge Worker] Failed to start:', error);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n console.error('[HarshJudge Worker] Fatal error:', err);\n process.exit(1);\n});\n"],"mappings":"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","names":["join","z","z","z","join","entries","dirname","join","fileURLToPath","__filename","__dirname"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@allenpan2026/harshjudge",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "AI-native E2E testing orchestration CLI for Claude Code.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"harshjudge": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"skills",
|
|
13
|
+
".claude-plugin",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup && vite build",
|
|
19
|
+
"build:cli": "tsup",
|
|
20
|
+
"build:ux": "vite build",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
25
|
+
"clean": "rm -rf dist dist-ux"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chokidar": "^3.6.0",
|
|
29
|
+
"commander": "^12.0.0",
|
|
30
|
+
"js-yaml": "^4.1.0",
|
|
31
|
+
"nanoid": "^5.0.9",
|
|
32
|
+
"react": "^18.3.1",
|
|
33
|
+
"react-dom": "^18.3.1",
|
|
34
|
+
"zod": "^3.23.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@testing-library/jest-dom": "^6.4.0",
|
|
38
|
+
"@testing-library/react": "^14.2.0",
|
|
39
|
+
"@types/js-yaml": "^4.0.9",
|
|
40
|
+
"@types/node": "^20.10.0",
|
|
41
|
+
"@types/react": "^18.3.0",
|
|
42
|
+
"@types/react-dom": "^18.3.0",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
45
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
46
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
47
|
+
"autoprefixer": "^10.4.18",
|
|
48
|
+
"eslint": "^8.57.0",
|
|
49
|
+
"eslint-config-prettier": "^9.1.0",
|
|
50
|
+
"jsdom": "^24.0.0",
|
|
51
|
+
"memfs": "^4.6.0",
|
|
52
|
+
"postcss": "^8.4.35",
|
|
53
|
+
"prettier": "^3.2.0",
|
|
54
|
+
"tailwindcss": "^3.4.1",
|
|
55
|
+
"tsup": "^8.5.1",
|
|
56
|
+
"typescript": "^5.3.0",
|
|
57
|
+
"vite": "^5.4.0",
|
|
58
|
+
"vitest": "^1.6.0"
|
|
59
|
+
},
|
|
60
|
+
"packageManager": "pnpm@9.14.4",
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harshjudge
|
|
3
|
+
description: AI-native E2E testing orchestration for Claude Code. Use when creating, running, or managing end-to-end test scenarios with visual evidence capture. Activates for tasks involving E2E tests, browser automation testing, test scenario creation, test execution with screenshots, or checking test status.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# HarshJudge E2E Testing
|
|
7
|
+
|
|
8
|
+
AI-native E2E testing with CLI commands and visual evidence capture.
|
|
9
|
+
|
|
10
|
+
## Core Principles
|
|
11
|
+
|
|
12
|
+
1. **Evidence First**: Screenshot before and after every action
|
|
13
|
+
2. **Fail Fast**: Stop on error, report with context
|
|
14
|
+
3. **Complete Runs**: Always call `harshjudge complete-run`, even on failure
|
|
15
|
+
4. **Step Isolation**: Each step executes in its own spawned agent for token efficiency
|
|
16
|
+
5. **Knowledge Accumulation**: Learnings go to `prd.md`, not scenarios
|
|
17
|
+
|
|
18
|
+
## Step-Based Execution
|
|
19
|
+
|
|
20
|
+
HarshJudge uses a **step-based agent pattern** for token-efficient test execution:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Main Agent Step Agents (spawned per step)
|
|
24
|
+
│
|
|
25
|
+
├─ harshjudge start <scenarioSlug>
|
|
26
|
+
│ ↓
|
|
27
|
+
│ Returns: runId, steps[]
|
|
28
|
+
│
|
|
29
|
+
├─► Spawn Agent: Step 01 ──────────────────────► Execute actions
|
|
30
|
+
│ │ │
|
|
31
|
+
│ │ ◄─────────────────────────────────── Return: { status, evidencePaths }
|
|
32
|
+
│ │
|
|
33
|
+
│ harshjudge complete-step <runId> --step 01 --status pass
|
|
34
|
+
│ │
|
|
35
|
+
├─► Spawn Agent: Step 02 ──────────────────────► Execute actions
|
|
36
|
+
│ │ │
|
|
37
|
+
│ │ ◄─────────────────────────────────── Return: { status, evidencePaths }
|
|
38
|
+
│ │
|
|
39
|
+
│ harshjudge complete-step <runId> --step 02 --status pass
|
|
40
|
+
│ │
|
|
41
|
+
│ ... (repeat for each step)
|
|
42
|
+
│
|
|
43
|
+
└─ harshjudge complete-run <runId> --status pass
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Benefits:**
|
|
47
|
+
- Each step agent has isolated context (no token accumulation)
|
|
48
|
+
- Large outputs (screenshots, logs) saved to files, not returned
|
|
49
|
+
- Main agent only receives concise summaries
|
|
50
|
+
- Automatic token optimization without manual management
|
|
51
|
+
|
|
52
|
+
## Workflows
|
|
53
|
+
|
|
54
|
+
| Intent | Reference | Key Commands |
|
|
55
|
+
|--------|-----------|-----------|
|
|
56
|
+
| Initialize project | [references/setup.md](references/setup.md) | `harshjudge init` |
|
|
57
|
+
| Create scenario | [references/create.md](references/create.md) | `harshjudge create` |
|
|
58
|
+
| Run scenario | [references/run.md](references/run.md) | `harshjudge start`, `harshjudge complete-step`, `harshjudge complete-run` |
|
|
59
|
+
| Fix failed test | [references/iterate.md](references/iterate.md) | `harshjudge status`, `harshjudge create` |
|
|
60
|
+
| Check status | [references/status.md](references/status.md) | `harshjudge status` |
|
|
61
|
+
|
|
62
|
+
## Project Structure
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
.harshJudge/
|
|
66
|
+
config.yaml # Project configuration
|
|
67
|
+
prd.md # Product requirements (from assets/prd.md template)
|
|
68
|
+
scenarios/{slug}/
|
|
69
|
+
meta.yaml # Scenario definition + run statistics
|
|
70
|
+
steps/ # Individual step files
|
|
71
|
+
01-step-slug.md # Step 01 details
|
|
72
|
+
02-step-slug.md # Step 02 details
|
|
73
|
+
...
|
|
74
|
+
runs/{runId}/ # Run history
|
|
75
|
+
result.json # Run result with per-step data
|
|
76
|
+
step-01/evidence/ # Step 01 evidence
|
|
77
|
+
step-02/evidence/ # Step 02 evidence
|
|
78
|
+
...
|
|
79
|
+
snapshots/ # Inspection tool outputs (token-saving pattern)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quick Reference
|
|
83
|
+
|
|
84
|
+
### HarshJudge CLI Commands
|
|
85
|
+
|
|
86
|
+
| Command | Purpose |
|
|
87
|
+
|---------|---------|
|
|
88
|
+
| `harshjudge init <name>` | Initialize project (creates .harshJudge/) |
|
|
89
|
+
| `harshjudge create <slug>` | Create/update scenario with step files |
|
|
90
|
+
| `harshjudge star <slug>` | Toggle/set scenario starred status |
|
|
91
|
+
| `harshjudge start <slug>` | Start test run, returns step list |
|
|
92
|
+
| `harshjudge evidence <runId>` | Capture evidence for a step |
|
|
93
|
+
| `harshjudge complete-step <runId>` | Complete a step, get next step ID |
|
|
94
|
+
| `harshjudge complete-run <runId>` | Finalize run with status |
|
|
95
|
+
| `harshjudge status [slug]` | Check project or scenario status |
|
|
96
|
+
| `harshjudge discover tree [path]` | Browse .harshJudge/ structure |
|
|
97
|
+
| `harshjudge discover search <pattern>` | Search file content |
|
|
98
|
+
| `harshjudge dashboard open/close/status` | Manage dashboard server |
|
|
99
|
+
|
|
100
|
+
### Playwright MCP Tools
|
|
101
|
+
|
|
102
|
+
| Tool | Purpose |
|
|
103
|
+
|------|---------|
|
|
104
|
+
| `browser_navigate` | Navigate to URL |
|
|
105
|
+
| `browser_snapshot` | Get accessibility tree (use before click/type) |
|
|
106
|
+
| `browser_click` | Click element using ref |
|
|
107
|
+
| `browser_type` | Type into input using ref |
|
|
108
|
+
| `browser_take_screenshot` | Capture screenshot for evidence |
|
|
109
|
+
| `browser_console_messages` | Get console logs |
|
|
110
|
+
| `browser_network_requests` | Get network activity |
|
|
111
|
+
| `browser_wait_for` | Wait for text/condition |
|
|
112
|
+
|
|
113
|
+
## Step Agent Prompt Template
|
|
114
|
+
|
|
115
|
+
When spawning an agent for each step:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Execute step {stepId} of scenario {scenarioSlug}:
|
|
119
|
+
|
|
120
|
+
## Step Content
|
|
121
|
+
{content from steps/{stepId}-{slug}.md}
|
|
122
|
+
|
|
123
|
+
## Project Context
|
|
124
|
+
Base URL: {from config.yaml}
|
|
125
|
+
Auth: {from prd.md if needed}
|
|
126
|
+
|
|
127
|
+
## Previous Step
|
|
128
|
+
Status: {pass|fail|first step}
|
|
129
|
+
|
|
130
|
+
## Your Task
|
|
131
|
+
1. Execute the actions using Playwright MCP tools
|
|
132
|
+
2. Use browser_snapshot before clicking to get element refs
|
|
133
|
+
3. Capture before/after screenshots using browser_take_screenshot
|
|
134
|
+
4. Record evidence: harshjudge evidence <runId> --step {stepNumber} --type screenshot --name before --data /path/to/screenshot.png
|
|
135
|
+
|
|
136
|
+
Return ONLY a JSON object:
|
|
137
|
+
{
|
|
138
|
+
"status": "pass" | "fail",
|
|
139
|
+
"evidencePaths": ["path1.png", "path2.png"],
|
|
140
|
+
"error": null | "error message"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
DO NOT return full evidence content. DO NOT explain your work.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Error Handling
|
|
147
|
+
|
|
148
|
+
On ANY error:
|
|
149
|
+
1. **STOP** - Do not proceed
|
|
150
|
+
2. **Report** - Command, params, error, resolution
|
|
151
|
+
3. **Check prd.md** - Is this a known pattern?
|
|
152
|
+
4. **Do NOT retry** - Unless user instructs
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Project PRD
|
|
2
|
+
|
|
3
|
+
## Application Type
|
|
4
|
+
<!-- backend | fullstack | frontend | other -->
|
|
5
|
+
{app_type}
|
|
6
|
+
|
|
7
|
+
## Ports
|
|
8
|
+
| Service | Port |
|
|
9
|
+
|---------|------|
|
|
10
|
+
| Frontend | {frontend_port} |
|
|
11
|
+
| Backend | {backend_port} |
|
|
12
|
+
| Database | {database_port} |
|
|
13
|
+
|
|
14
|
+
## Main Scenarios
|
|
15
|
+
<!-- High-level list of main testing scenarios -->
|
|
16
|
+
- {scenario_1}
|
|
17
|
+
- {scenario_2}
|
|
18
|
+
- {scenario_3}
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
<!-- Auth requirements for testing -->
|
|
22
|
+
- **Login URL:** {login_url}
|
|
23
|
+
- **Test Credentials:**
|
|
24
|
+
- Username: {test_username}
|
|
25
|
+
- Password: {test_password}
|
|
26
|
+
|
|
27
|
+
## Tech Stack
|
|
28
|
+
<!-- Frameworks, libraries, tools -->
|
|
29
|
+
- Frontend: {frontend_stack}
|
|
30
|
+
- Backend: {backend_stack}
|
|
31
|
+
- Testing: {testing_tools}
|
|
32
|
+
|
|
33
|
+
## Notes
|
|
34
|
+
<!-- Additional context for test scenarios -->
|
|
35
|
+
- {note_1}
|
|
36
|
+
- {note_2}
|