panopticon-cli 0.3.2 → 0.3.4

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/README.md CHANGED
@@ -340,6 +340,44 @@ Cloister manages specialized agents that handle specific phases of the developme
340
340
  | **review-agent** | Code review before merge | After tests pass (manual trigger) |
341
341
  | **merge-agent** | Handles git merge and conflict resolution | "Approve & Merge" button |
342
342
 
343
+ #### Merge Agent Workflow
344
+
345
+ The merge-agent is a specialist that handles **ALL** merges, not just conflicts. This ensures:
346
+ - It sees all code changes coming through the pipeline
347
+ - It builds context about the codebase over time
348
+ - When conflicts DO occur, it has better understanding for intelligent resolution
349
+ - Tests are always run before completing the merge
350
+
351
+ **Workflow:**
352
+
353
+ 1. **Pull latest main** - Ensures local main is up-to-date
354
+ 2. **Analyze incoming changes** - Reviews what the feature branch contains
355
+ 3. **Perform merge** - Merges feature branch into main
356
+ 4. **Resolve conflicts** - If conflicts exist, uses AI to resolve them intelligently
357
+ 5. **Run tests** - Verifies the merge didn't break anything
358
+ 6. **Commit merge** - Commits the merge with descriptive message
359
+ 7. **Report results** - Returns success/failure with details
360
+
361
+ **Triggering merge-agent:**
362
+
363
+ ```bash
364
+ # Via dashboard - click "Approve & Merge" on an issue card
365
+ # merge-agent is ALWAYS invoked, regardless of whether conflicts exist
366
+
367
+ # Via CLI
368
+ pan specialists wake merge-agent --issue MIN-123
369
+ ```
370
+
371
+ The merge-agent uses a specialized prompt template that instructs it to:
372
+ - Never force-push
373
+ - Always run tests before completing
374
+ - Document conflict resolution decisions
375
+ - Provide detailed feedback on what was merged
376
+
377
+ #### Specialist Auto-Initialization
378
+
379
+ When Cloister starts, it automatically initializes specialists that don't exist yet. This ensures the test-agent, review-agent, and merge-agent are ready to receive wake signals without manual setup.
380
+
343
381
  ### Automatic Handoffs
344
382
 
345
383
  Cloister detects situations that require intervention:
@@ -349,8 +387,55 @@ Cloister detects situations that require intervention:
349
387
  | **stuck_escalation** | No activity for 30+ minutes | Escalate to more capable model |
350
388
  | **complexity_upgrade** | Task complexity exceeds model capability | Route to Opus |
351
389
  | **implementation_complete** | Agent signals work is done | Wake test-agent |
390
+ | **test_failure** | Tests fail repeatedly | Escalate model or request help |
391
+ | **planning_complete** | Planning session finishes | Transition to implementation |
352
392
  | **merge_requested** | User clicks "Approve & Merge" | Wake merge-agent |
353
393
 
394
+ ### Handoff Methods
395
+
396
+ Cloister supports two handoff methods, automatically selected based on agent type:
397
+
398
+ | Method | When Used | How It Works |
399
+ |--------|-----------|--------------|
400
+ | **Kill & Spawn** | General agents (agent-min-123, etc.) | 1. Captures full context (STATE.md, beads, git state)<br>2. Kills tmux session<br>3. Spawns new agent with handoff prompt<br>4. New agent continues work with preserved context |
401
+ | **Specialist Wake** | Permanent specialists (merge-agent, test-agent) | 1. Captures handoff context<br>2. Sends wake message to existing session<br>3. Specialist resumes with context injection |
402
+
403
+ **Kill & Spawn** is used for temporary agents that work on specific issues. It creates a clean handoff by:
404
+ - Capturing the agent's current understanding (from STATE.md)
405
+ - Preserving beads task progress and open items
406
+ - Including relevant git diff and file context
407
+ - Building a comprehensive handoff prompt for the new model
408
+
409
+ **Specialist Wake** is used for permanent specialists that persist across multiple issues. It avoids the overhead of killing/respawning by injecting context into the existing session.
410
+
411
+ ### Handoff Context Capture
412
+
413
+ When a handoff occurs, Cloister captures:
414
+
415
+ ```json
416
+ {
417
+ "agentId": "agent-min-123",
418
+ "issueId": "MIN-123",
419
+ "currentModel": "sonnet",
420
+ "targetModel": "opus",
421
+ "reason": "stuck_escalation",
422
+ "handoffCount": 1,
423
+ "state": {
424
+ "phase": "implementation",
425
+ "complexity": "complex",
426
+ "lastActivity": "2024-01-22T10:30:00-08:00"
427
+ },
428
+ "beadsTasks": [...],
429
+ "gitContext": {
430
+ "branch": "feature/min-123",
431
+ "uncommittedChanges": ["src/auth.ts", "src/tests/auth.test.ts"],
432
+ "recentCommits": [...]
433
+ }
434
+ }
435
+ ```
436
+
437
+ Handoff prompts are saved to `~/.panopticon/agents/{agent-id}/handoffs/` for debugging.
438
+
354
439
  ### Heartbeat Monitoring
355
440
 
356
441
  Agents send heartbeats via Claude Code hooks. Cloister tracks:
@@ -373,6 +458,41 @@ Heartbeat files are stored in `~/.panopticon/heartbeats/`:
373
458
  }
374
459
  ```
375
460
 
461
+ ### Heartbeat Hook Installation
462
+
463
+ The heartbeat hook is automatically synced to `~/.panopticon/bin/heartbeat-hook` via `pan sync`. It's also installed automatically when you install or upgrade Panopticon via npm.
464
+
465
+ **Manual installation:**
466
+ ```bash
467
+ pan sync # Syncs all skills, agents, AND hooks
468
+ ```
469
+
470
+ **Hook configuration in `~/.claude/settings.json`:**
471
+ ```json
472
+ {
473
+ "hooks": {
474
+ "PostToolUse": [
475
+ {
476
+ "matcher": "*",
477
+ "hooks": [
478
+ {
479
+ "type": "command",
480
+ "command": "~/.panopticon/bin/heartbeat-hook"
481
+ }
482
+ ]
483
+ }
484
+ ]
485
+ }
486
+ }
487
+ ```
488
+
489
+ **Hook resilience:** The heartbeat hook is designed to fail silently if:
490
+ - The heartbeats directory doesn't exist
491
+ - Write permissions are missing
492
+ - The hook script has errors
493
+
494
+ This prevents hook failures from interrupting agent work.
495
+
376
496
  ### Configuration
377
497
 
378
498
  Cloister configuration lives in `~/.panopticon/cloister/config.json`:
@@ -398,6 +518,77 @@ Cloister configuration lives in `~/.panopticon/cloister/config.json`:
398
518
 
399
519
  ---
400
520
 
521
+ ## Model Routing & Complexity Detection
522
+
523
+ Cloister automatically routes tasks to the appropriate model based on detected complexity, optimizing for cost while ensuring quality.
524
+
525
+ ### Complexity Levels
526
+
527
+ | Level | Model | Use Case |
528
+ |-------|-------|----------|
529
+ | **trivial** | Haiku | Typos, comments, documentation updates |
530
+ | **simple** | Haiku | Small fixes, test additions, minor changes |
531
+ | **medium** | Sonnet | Features, components, integrations |
532
+ | **complex** | Sonnet/Opus | Refactors, migrations, redesigns |
533
+ | **expert** | Opus | Architecture, security, performance optimization |
534
+
535
+ ### Complexity Detection Signals
536
+
537
+ Complexity is detected from multiple signals (in priority order):
538
+
539
+ 1. **Explicit field** - Task has a `complexity` field set (e.g., in beads)
540
+ 2. **Labels/tags** - Issue labels like `architecture`, `security`, `refactor`
541
+ 3. **Keywords** - Title/description contains keywords like "migration", "overhaul"
542
+ 4. **File count** - Number of files changed (>20 files = complex)
543
+ 5. **Time estimate** - If estimate exceeds thresholds
544
+
545
+ **Keyword patterns:**
546
+ ```javascript
547
+ {
548
+ trivial: ['typo', 'rename', 'comment', 'documentation', 'readme'],
549
+ simple: ['add comment', 'update docs', 'fix typo', 'small fix'],
550
+ medium: ['feature', 'endpoint', 'component', 'service'],
551
+ complex: ['refactor', 'migration', 'redesign', 'overhaul'],
552
+ expert: ['architecture', 'security', 'performance optimization']
553
+ }
554
+ ```
555
+
556
+ ### Configuring Model Routing
557
+
558
+ Edit `~/.panopticon/cloister/config.json`:
559
+
560
+ ```json
561
+ {
562
+ "model_selection": {
563
+ "default_model": "sonnet",
564
+ "complexity_routing": {
565
+ "trivial": "haiku",
566
+ "simple": "haiku",
567
+ "medium": "sonnet",
568
+ "complex": "sonnet",
569
+ "expert": "opus"
570
+ }
571
+ }
572
+ }
573
+ ```
574
+
575
+ ### Cost Optimization
576
+
577
+ Model routing helps optimize costs:
578
+
579
+ | Model | Relative Cost | Best For |
580
+ |-------|---------------|----------|
581
+ | Haiku | 1x (cheapest) | Simple tasks, bulk operations |
582
+ | Sonnet | 3x | Most development work |
583
+ | Opus | 15x | Complex architecture, critical fixes |
584
+
585
+ A typical agent run might:
586
+ 1. Start on Haiku for initial exploration
587
+ 2. Escalate to Sonnet for implementation
588
+ 3. Escalate to Opus only if stuck or complexity detected
589
+
590
+ ---
591
+
401
592
  ## Multi-Project Support
402
593
 
403
594
  Panopticon supports managing multiple projects with intelligent issue routing.
@@ -462,9 +653,22 @@ pan sync --dry-run # Preview what will be synced
462
653
  pan doctor # Check system health
463
654
  pan skills # List available skills
464
655
  pan status # Show running agents
656
+ pan up # Start dashboard (Docker or minimal)
657
+ pan down # Stop dashboard and services
465
658
  ```
466
659
 
467
- > **Note:** `pan sync` now automatically syncs heartbeat hooks to `~/.panopticon/bin/`. This happens automatically on `npm install/upgrade` as well.
660
+ #### What `pan sync` Does
661
+
662
+ `pan sync` synchronizes Panopticon assets to all supported AI tools:
663
+
664
+ | Asset Type | Source | Destinations |
665
+ |------------|--------|--------------|
666
+ | **Skills** | `~/.panopticon/skills/` | `~/.claude/skills/`, `~/.codex/skills/`, `~/.gemini/skills/` |
667
+ | **Agents** | `~/.panopticon/agents/*.md` | `~/.claude/agents/` |
668
+ | **Commands** | `~/.panopticon/commands/` | `~/.claude/commands/` |
669
+ | **Hooks** | `src/hooks/` (in package) | `~/.panopticon/bin/` |
670
+
671
+ **Automatic sync:** Hooks are also synced automatically when you install or upgrade Panopticon via npm (`postinstall` hook).
468
672
 
469
673
  ### Agent Management
470
674
 
@@ -1029,6 +1233,7 @@ This ensures every Panopticon-managed project has a well-defined canonical PRD t
1029
1233
  hook.json # GUPP work queue
1030
1234
  cv.json # Work history
1031
1235
  mail/ # Incoming messages
1236
+ handoffs/ # Handoff prompts (for debugging)
1032
1237
 
1033
1238
  cloister/ # Cloister AI lifecycle manager
1034
1239
  config.json # Cloister settings
@@ -1038,6 +1243,9 @@ This ensures every Panopticon-managed project has a well-defined canonical PRD t
1038
1243
  heartbeats/ # Real-time agent activity
1039
1244
  agent-min-123.json # Last heartbeat from agent
1040
1245
 
1246
+ logs/ # Log files
1247
+ handoffs.jsonl # All handoff events (for analytics)
1248
+
1041
1249
  costs/ # Raw cost logs (JSONL)
1042
1250
  backups/ # Sync backups
1043
1251
  traefik/ # Traefik reverse proxy config
@@ -1045,6 +1253,36 @@ This ensures every Panopticon-managed project has a well-defined canonical PRD t
1045
1253
  certs/ # TLS certificates
1046
1254
  ```
1047
1255
 
1256
+ ### Agent State Management
1257
+
1258
+ Each agent's state is tracked in `~/.panopticon/agents/{agent-id}/state.json`:
1259
+
1260
+ ```json
1261
+ {
1262
+ "id": "agent-min-123",
1263
+ "issueId": "MIN-123",
1264
+ "workspace": "/home/user/projects/myapp/workspaces/feature-min-123",
1265
+ "branch": "feature/min-123",
1266
+ "phase": "implementation",
1267
+ "model": "sonnet",
1268
+ "complexity": "medium",
1269
+ "handoffCount": 0,
1270
+ "sessionId": "abc123",
1271
+ "createdAt": "2024-01-22T10:00:00-08:00",
1272
+ "updatedAt": "2024-01-22T10:30:00-08:00"
1273
+ }
1274
+ ```
1275
+
1276
+ | Field | Description |
1277
+ |-------|-------------|
1278
+ | `phase` | Current work phase: `planning`, `implementation`, `testing`, `review`, `merging` |
1279
+ | `model` | Current model: `haiku`, `sonnet`, `opus` |
1280
+ | `complexity` | Detected complexity: `trivial`, `simple`, `medium`, `complex`, `expert` |
1281
+ | `handoffCount` | Number of times the agent has been handed off to a different model |
1282
+ | `sessionId` | Claude Code session ID (for resuming after handoff) |
1283
+
1284
+ **State Cleanup:** When an agent is killed or aborted (`pan work kill`), Panopticon automatically cleans up its state files to prevent stale data from affecting future runs.
1285
+
1048
1286
  ## Health Monitoring (Deacon Pattern)
1049
1287
 
1050
1288
  Panopticon implements the Deacon pattern for stuck agent detection:
@@ -1,12 +1,8 @@
1
1
  import {
2
- PANOPTICON_HOME,
3
- init_esm_shims,
4
- init_paths
5
- } from "./chunk-SG7O6I7R.js";
2
+ PANOPTICON_HOME
3
+ } from "./chunk-P5TQ5C3J.js";
6
4
 
7
5
  // src/lib/projects.ts
8
- init_esm_shims();
9
- init_paths();
10
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
11
7
  import { join } from "path";
12
8
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
@@ -174,4 +170,4 @@ export {
174
170
  createDefaultProjectsConfig,
175
171
  initializeProjectsConfig
176
172
  };
177
- //# sourceMappingURL=chunk-PSJRCUOA.js.map
173
+ //# sourceMappingURL=chunk-BH6BR26M.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/projects.ts"],"sourcesContent":["/**\n * Project Registry - Multi-project support for Panopticon\n *\n * Maps Linear team prefixes and labels to project paths for workspace creation.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { PANOPTICON_HOME } from './paths.js';\n\nexport const PROJECTS_CONFIG_FILE = join(PANOPTICON_HOME, 'projects.yaml');\n\n/**\n * Issue routing rule - routes issues with certain labels to specific paths\n */\nexport interface IssueRoutingRule {\n labels?: string[];\n default?: boolean;\n path: string;\n}\n\n/**\n * Project configuration\n */\nexport interface ProjectConfig {\n name: string;\n path: string;\n linear_team?: string;\n issue_routing?: IssueRoutingRule[];\n}\n\n/**\n * Full projects configuration file\n */\nexport interface ProjectsConfig {\n projects: Record<string, ProjectConfig>;\n}\n\n/**\n * Resolved project info for workspace creation\n */\nexport interface ResolvedProject {\n projectKey: string;\n projectName: string;\n projectPath: string;\n linearTeam?: string;\n}\n\n/**\n * Load projects configuration from ~/.panopticon/projects.yaml\n */\nexport function loadProjectsConfig(): ProjectsConfig {\n if (!existsSync(PROJECTS_CONFIG_FILE)) {\n return { projects: {} };\n }\n\n try {\n const content = readFileSync(PROJECTS_CONFIG_FILE, 'utf-8');\n const config = parseYaml(content) as ProjectsConfig;\n return config || { projects: {} };\n } catch (error: any) {\n console.error(`Failed to parse projects.yaml: ${error.message}`);\n return { projects: {} };\n }\n}\n\n/**\n * Save projects configuration\n */\nexport function saveProjectsConfig(config: ProjectsConfig): void {\n const dir = PANOPTICON_HOME;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const yaml = stringifyYaml(config, { indent: 2 });\n writeFileSync(PROJECTS_CONFIG_FILE, yaml, 'utf-8');\n}\n\n/**\n * Get a list of all registered projects\n */\nexport function listProjects(): Array<{ key: string; config: ProjectConfig }> {\n const config = loadProjectsConfig();\n return Object.entries(config.projects).map(([key, projectConfig]) => ({\n key,\n config: projectConfig,\n }));\n}\n\n/**\n * Add or update a project in the registry\n */\nexport function registerProject(key: string, projectConfig: ProjectConfig): void {\n const config = loadProjectsConfig();\n config.projects[key] = projectConfig;\n saveProjectsConfig(config);\n}\n\n/**\n * Remove a project from the registry\n */\nexport function unregisterProject(key: string): boolean {\n const config = loadProjectsConfig();\n if (config.projects[key]) {\n delete config.projects[key];\n saveProjectsConfig(config);\n return true;\n }\n return false;\n}\n\n/**\n * Extract Linear team prefix from an issue ID\n * E.g., \"MIN-123\" -> \"MIN\", \"PAN-456\" -> \"PAN\"\n */\nexport function extractTeamPrefix(issueId: string): string | null {\n const match = issueId.match(/^([A-Z]+)-\\d+$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\n/**\n * Find project by Linear team prefix\n */\nexport function findProjectByTeam(teamPrefix: string): ProjectConfig | null {\n const config = loadProjectsConfig();\n\n for (const [, projectConfig] of Object.entries(config.projects)) {\n if (projectConfig.linear_team?.toUpperCase() === teamPrefix.toUpperCase()) {\n return projectConfig;\n }\n }\n\n return null;\n}\n\n/**\n * Resolve the correct project path for an issue based on labels\n *\n * @param project - The project config\n * @param labels - Array of label names from the Linear issue\n * @returns The resolved path (may differ from project.path based on routing rules)\n */\nexport function resolveProjectPath(project: ProjectConfig, labels: string[] = []): string {\n if (!project.issue_routing || project.issue_routing.length === 0) {\n return project.path;\n }\n\n // Normalize labels to lowercase for comparison\n const normalizedLabels = labels.map(l => l.toLowerCase());\n\n // First, check label-based routing rules\n for (const rule of project.issue_routing) {\n if (rule.labels && rule.labels.length > 0) {\n const ruleLabels = rule.labels.map(l => l.toLowerCase());\n const hasMatch = ruleLabels.some(label => normalizedLabels.includes(label));\n if (hasMatch) {\n return rule.path;\n }\n }\n }\n\n // Then, find default rule\n for (const rule of project.issue_routing) {\n if (rule.default) {\n return rule.path;\n }\n }\n\n // Fall back to project path\n return project.path;\n}\n\n/**\n * Resolve project from an issue ID (and optional labels)\n *\n * @param issueId - Linear issue ID (e.g., \"MIN-123\")\n * @param labels - Optional array of label names\n * @returns Resolved project info or null if not found\n */\nexport function resolveProjectFromIssue(\n issueId: string,\n labels: string[] = []\n): ResolvedProject | null {\n const teamPrefix = extractTeamPrefix(issueId);\n if (!teamPrefix) {\n return null;\n }\n\n const config = loadProjectsConfig();\n\n // Find project by team prefix\n for (const [key, projectConfig] of Object.entries(config.projects)) {\n if (projectConfig.linear_team?.toUpperCase() === teamPrefix) {\n const resolvedPath = resolveProjectPath(projectConfig, labels);\n return {\n projectKey: key,\n projectName: projectConfig.name,\n projectPath: resolvedPath,\n linearTeam: projectConfig.linear_team,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Get a project by key\n */\nexport function getProject(key: string): ProjectConfig | null {\n const config = loadProjectsConfig();\n return config.projects[key] || null;\n}\n\n/**\n * Check if projects.yaml exists and has any projects\n */\nexport function hasProjects(): boolean {\n const config = loadProjectsConfig();\n return Object.keys(config.projects).length > 0;\n}\n\n/**\n * Create a default projects.yaml with example structure\n */\nexport function createDefaultProjectsConfig(): ProjectsConfig {\n const defaultConfig: ProjectsConfig = {\n projects: {\n // Example project - commented out in actual file\n },\n };\n\n return defaultConfig;\n}\n\n/**\n * Initialize projects.yaml with example configuration\n */\nexport function initializeProjectsConfig(): void {\n if (existsSync(PROJECTS_CONFIG_FILE)) {\n console.log(`Projects config already exists at ${PROJECTS_CONFIG_FILE}`);\n return;\n }\n\n const exampleYaml = `# Panopticon Project Registry\n# Maps Linear teams to project paths for workspace creation\n\nprojects:\n # Example: Mind Your Now project\n # myn:\n # name: \"Mind Your Now\"\n # path: /home/user/projects/myn\n # linear_team: MIN\n # issue_routing:\n # # Route docs/marketing issues to docs repo\n # - labels: [docs, marketing, seo, landing-pages]\n # path: /home/user/projects/myn/docs\n # # Default: main repo\n # - default: true\n # path: /home/user/projects/myn\n\n # Example: Panopticon itself\n # panopticon:\n # name: \"Panopticon\"\n # path: /home/user/projects/panopticon\n # linear_team: PAN\n`;\n\n const dir = PANOPTICON_HOME;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n writeFileSync(PROJECTS_CONFIG_FILE, exampleYaml, 'utf-8');\n console.log(`Created example projects config at ${PROJECTS_CONFIG_FILE}`);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/lib/projects.ts"],"sourcesContent":["/**\n * Project Registry - Multi-project support for Panopticon\n *\n * Maps Linear team prefixes and labels to project paths for workspace creation.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { PANOPTICON_HOME } from './paths.js';\n\nexport const PROJECTS_CONFIG_FILE = join(PANOPTICON_HOME, 'projects.yaml');\n\n/**\n * Issue routing rule - routes issues with certain labels to specific paths\n */\nexport interface IssueRoutingRule {\n labels?: string[];\n default?: boolean;\n path: string;\n}\n\n/**\n * Project configuration\n */\nexport interface ProjectConfig {\n name: string;\n path: string;\n linear_team?: string;\n issue_routing?: IssueRoutingRule[];\n}\n\n/**\n * Full projects configuration file\n */\nexport interface ProjectsConfig {\n projects: Record<string, ProjectConfig>;\n}\n\n/**\n * Resolved project info for workspace creation\n */\nexport interface ResolvedProject {\n projectKey: string;\n projectName: string;\n projectPath: string;\n linearTeam?: string;\n}\n\n/**\n * Load projects configuration from ~/.panopticon/projects.yaml\n */\nexport function loadProjectsConfig(): ProjectsConfig {\n if (!existsSync(PROJECTS_CONFIG_FILE)) {\n return { projects: {} };\n }\n\n try {\n const content = readFileSync(PROJECTS_CONFIG_FILE, 'utf-8');\n const config = parseYaml(content) as ProjectsConfig;\n return config || { projects: {} };\n } catch (error: any) {\n console.error(`Failed to parse projects.yaml: ${error.message}`);\n return { projects: {} };\n }\n}\n\n/**\n * Save projects configuration\n */\nexport function saveProjectsConfig(config: ProjectsConfig): void {\n const dir = PANOPTICON_HOME;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const yaml = stringifyYaml(config, { indent: 2 });\n writeFileSync(PROJECTS_CONFIG_FILE, yaml, 'utf-8');\n}\n\n/**\n * Get a list of all registered projects\n */\nexport function listProjects(): Array<{ key: string; config: ProjectConfig }> {\n const config = loadProjectsConfig();\n return Object.entries(config.projects).map(([key, projectConfig]) => ({\n key,\n config: projectConfig,\n }));\n}\n\n/**\n * Add or update a project in the registry\n */\nexport function registerProject(key: string, projectConfig: ProjectConfig): void {\n const config = loadProjectsConfig();\n config.projects[key] = projectConfig;\n saveProjectsConfig(config);\n}\n\n/**\n * Remove a project from the registry\n */\nexport function unregisterProject(key: string): boolean {\n const config = loadProjectsConfig();\n if (config.projects[key]) {\n delete config.projects[key];\n saveProjectsConfig(config);\n return true;\n }\n return false;\n}\n\n/**\n * Extract Linear team prefix from an issue ID\n * E.g., \"MIN-123\" -> \"MIN\", \"PAN-456\" -> \"PAN\"\n */\nexport function extractTeamPrefix(issueId: string): string | null {\n const match = issueId.match(/^([A-Z]+)-\\d+$/i);\n return match ? match[1].toUpperCase() : null;\n}\n\n/**\n * Find project by Linear team prefix\n */\nexport function findProjectByTeam(teamPrefix: string): ProjectConfig | null {\n const config = loadProjectsConfig();\n\n for (const [, projectConfig] of Object.entries(config.projects)) {\n if (projectConfig.linear_team?.toUpperCase() === teamPrefix.toUpperCase()) {\n return projectConfig;\n }\n }\n\n return null;\n}\n\n/**\n * Resolve the correct project path for an issue based on labels\n *\n * @param project - The project config\n * @param labels - Array of label names from the Linear issue\n * @returns The resolved path (may differ from project.path based on routing rules)\n */\nexport function resolveProjectPath(project: ProjectConfig, labels: string[] = []): string {\n if (!project.issue_routing || project.issue_routing.length === 0) {\n return project.path;\n }\n\n // Normalize labels to lowercase for comparison\n const normalizedLabels = labels.map(l => l.toLowerCase());\n\n // First, check label-based routing rules\n for (const rule of project.issue_routing) {\n if (rule.labels && rule.labels.length > 0) {\n const ruleLabels = rule.labels.map(l => l.toLowerCase());\n const hasMatch = ruleLabels.some(label => normalizedLabels.includes(label));\n if (hasMatch) {\n return rule.path;\n }\n }\n }\n\n // Then, find default rule\n for (const rule of project.issue_routing) {\n if (rule.default) {\n return rule.path;\n }\n }\n\n // Fall back to project path\n return project.path;\n}\n\n/**\n * Resolve project from an issue ID (and optional labels)\n *\n * @param issueId - Linear issue ID (e.g., \"MIN-123\")\n * @param labels - Optional array of label names\n * @returns Resolved project info or null if not found\n */\nexport function resolveProjectFromIssue(\n issueId: string,\n labels: string[] = []\n): ResolvedProject | null {\n const teamPrefix = extractTeamPrefix(issueId);\n if (!teamPrefix) {\n return null;\n }\n\n const config = loadProjectsConfig();\n\n // Find project by team prefix\n for (const [key, projectConfig] of Object.entries(config.projects)) {\n if (projectConfig.linear_team?.toUpperCase() === teamPrefix) {\n const resolvedPath = resolveProjectPath(projectConfig, labels);\n return {\n projectKey: key,\n projectName: projectConfig.name,\n projectPath: resolvedPath,\n linearTeam: projectConfig.linear_team,\n };\n }\n }\n\n return null;\n}\n\n/**\n * Get a project by key\n */\nexport function getProject(key: string): ProjectConfig | null {\n const config = loadProjectsConfig();\n return config.projects[key] || null;\n}\n\n/**\n * Check if projects.yaml exists and has any projects\n */\nexport function hasProjects(): boolean {\n const config = loadProjectsConfig();\n return Object.keys(config.projects).length > 0;\n}\n\n/**\n * Create a default projects.yaml with example structure\n */\nexport function createDefaultProjectsConfig(): ProjectsConfig {\n const defaultConfig: ProjectsConfig = {\n projects: {\n // Example project - commented out in actual file\n },\n };\n\n return defaultConfig;\n}\n\n/**\n * Initialize projects.yaml with example configuration\n */\nexport function initializeProjectsConfig(): void {\n if (existsSync(PROJECTS_CONFIG_FILE)) {\n console.log(`Projects config already exists at ${PROJECTS_CONFIG_FILE}`);\n return;\n }\n\n const exampleYaml = `# Panopticon Project Registry\n# Maps Linear teams to project paths for workspace creation\n\nprojects:\n # Example: Mind Your Now project\n # myn:\n # name: \"Mind Your Now\"\n # path: /home/user/projects/myn\n # linear_team: MIN\n # issue_routing:\n # # Route docs/marketing issues to docs repo\n # - labels: [docs, marketing, seo, landing-pages]\n # path: /home/user/projects/myn/docs\n # # Default: main repo\n # - default: true\n # path: /home/user/projects/myn\n\n # Example: Panopticon itself\n # panopticon:\n # name: \"Panopticon\"\n # path: /home/user/projects/panopticon\n # linear_team: PAN\n`;\n\n const dir = PANOPTICON_HOME;\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n writeFileSync(PROJECTS_CONFIG_FILE, exampleYaml, 'utf-8');\n console.log(`Created example projects config at ${PROJECTS_CONFIG_FILE}`);\n}\n"],"mappings":"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","names":[]}
@@ -6,14 +6,10 @@ import {
6
6
  CONFIG_FILE,
7
7
  SKILLS_DIR,
8
8
  SOURCE_SCRIPTS_DIR,
9
- SYNC_TARGETS,
10
- init_esm_shims,
11
- init_paths
12
- } from "./chunk-SG7O6I7R.js";
9
+ SYNC_TARGETS
10
+ } from "./chunk-P5TQ5C3J.js";
13
11
 
14
12
  // src/lib/config.ts
15
- init_esm_shims();
16
- init_paths();
17
13
  import { readFileSync, writeFileSync, existsSync } from "fs";
18
14
  import { parse, stringify } from "@iarna/toml";
19
15
  var DEFAULT_CONFIG = {
@@ -74,7 +70,6 @@ function getDefaultConfig() {
74
70
  }
75
71
 
76
72
  // src/lib/shell.ts
77
- init_esm_shims();
78
73
  import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
79
74
  import { homedir } from "os";
80
75
  import { join } from "path";
@@ -126,8 +121,6 @@ function getAliasInstructions(shell) {
126
121
  }
127
122
 
128
123
  // src/lib/backup.ts
129
- init_esm_shims();
130
- init_paths();
131
124
  import { existsSync as existsSync3, mkdirSync, readdirSync, cpSync, rmSync } from "fs";
132
125
  import { join as join2, basename } from "path";
133
126
  function createBackupTimestamp() {
@@ -194,8 +187,6 @@ function cleanOldBackups(keepCount = 10) {
194
187
  }
195
188
 
196
189
  // src/lib/sync.ts
197
- init_esm_shims();
198
- init_paths();
199
190
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, symlinkSync, unlinkSync, lstatSync, readlinkSync, rmSync as rmSync2, copyFileSync, chmodSync } from "fs";
200
191
  import { join as join3 } from "path";
201
192
  function removeTarget(targetPath) {
@@ -379,7 +370,6 @@ function syncHooks() {
379
370
  }
380
371
 
381
372
  // src/lib/tracker/interface.ts
382
- init_esm_shims();
383
373
  var NotImplementedError = class extends Error {
384
374
  constructor(feature) {
385
375
  super(`Not implemented: ${feature}`);
@@ -400,7 +390,6 @@ var TrackerAuthError = class extends Error {
400
390
  };
401
391
 
402
392
  // src/lib/tracker/linear.ts
403
- init_esm_shims();
404
393
  import { LinearClient } from "@linear/sdk";
405
394
  var STATE_MAP = {
406
395
  backlog: "open",
@@ -610,7 +599,6 @@ var LinearTracker = class {
610
599
  };
611
600
 
612
601
  // src/lib/tracker/github.ts
613
- init_esm_shims();
614
602
  import { Octokit } from "@octokit/rest";
615
603
  var GitHubTracker = class {
616
604
  name = "github";
@@ -771,7 +759,6 @@ var GitHubTracker = class {
771
759
  };
772
760
 
773
761
  // src/lib/tracker/gitlab.ts
774
- init_esm_shims();
775
762
  var GitLabTracker = class {
776
763
  constructor(token, projectId) {
777
764
  this.token = token;
@@ -820,11 +807,7 @@ var GitLabTracker = class {
820
807
  }
821
808
  };
822
809
 
823
- // src/lib/tracker/factory.ts
824
- init_esm_shims();
825
-
826
810
  // src/lib/tracker/rally.ts
827
- init_esm_shims();
828
811
  import rally from "rally";
829
812
  var STATE_MAP2 = {
830
813
  Defined: "open",
@@ -1287,7 +1270,6 @@ function getAllTrackers(trackersConfig) {
1287
1270
  }
1288
1271
 
1289
1272
  // src/lib/tracker/linking.ts
1290
- init_esm_shims();
1291
1273
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
1292
1274
  import { join as join4 } from "path";
1293
1275
  import { homedir as homedir2 } from "os";
@@ -1428,9 +1410,6 @@ function getLinkManager() {
1428
1410
  return _linkManager;
1429
1411
  }
1430
1412
 
1431
- // src/lib/tracker/index.ts
1432
- init_esm_shims();
1433
-
1434
1413
  export {
1435
1414
  loadConfig,
1436
1415
  saveConfig,
@@ -1466,4 +1445,4 @@ export {
1466
1445
  LinkManager,
1467
1446
  getLinkManager
1468
1447
  };
1469
- //# sourceMappingURL=chunk-B2JBBOJN.js.map
1448
+ //# sourceMappingURL=chunk-C6A7S65K.js.map