From 9e3d11f83d33a4c588672c30463874338f50c970 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:39:37 -0400 Subject: [PATCH 1/8] refactor: move orphan skills into Utilities/ category MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moved 6 flat-level skills into Utilities/ for PAI 4.0.3 parity: - AudioEditor → Utilities/AudioEditor (was missing from Utilities) - CodeReview → Utilities/CodeReview (OpenCode-specific) - OpenCodeSystem → Utilities/OpenCodeSystem (OpenCode-specific) - Sales → Utilities/Sales (OpenCode-specific) - System → Utilities/System (OpenCode-specific) - WriteStory → Utilities/WriteStory (OpenCode-specific) OpenCode-specific skills preserved in Utilities/ category. --- .opencode/skills/{ => Utilities}/AudioEditor/SKILL.md | 0 .../skills/{ => Utilities}/AudioEditor/Tools/Analyze.help.md | 0 .opencode/skills/{ => Utilities}/AudioEditor/Tools/Analyze.ts | 0 .opencode/skills/{ => Utilities}/AudioEditor/Tools/Edit.help.md | 0 .opencode/skills/{ => Utilities}/AudioEditor/Tools/Edit.ts | 0 .../skills/{ => Utilities}/AudioEditor/Tools/Pipeline.help.md | 0 .opencode/skills/{ => Utilities}/AudioEditor/Tools/Pipeline.ts | 0 .opencode/skills/{ => Utilities}/AudioEditor/Tools/Polish.help.md | 0 .opencode/skills/{ => Utilities}/AudioEditor/Tools/Polish.ts | 0 .../skills/{ => Utilities}/AudioEditor/Tools/Transcribe.help.md | 0 .opencode/skills/{ => Utilities}/AudioEditor/Tools/Transcribe.ts | 0 .opencode/skills/{ => Utilities}/AudioEditor/Workflows/Clean.md | 0 .opencode/skills/{ => Utilities}/CodeReview/SKILL.md | 0 .opencode/skills/{ => Utilities}/OpenCodeSystem/SKILL.md | 0 .opencode/skills/{ => Utilities}/Sales/SKILL.md | 0 .../skills/{ => Utilities}/Sales/Workflows/CreateNarrative.md | 0 .../skills/{ => Utilities}/Sales/Workflows/CreateSalesPackage.md | 0 .opencode/skills/{ => Utilities}/Sales/Workflows/CreateVisual.md | 0 .opencode/skills/{ => Utilities}/System/SKILL.md | 0 .opencode/skills/{ => Utilities}/System/Tools/CreateUpdate.ts | 0 .opencode/skills/{ => Utilities}/System/Tools/SecretScan.ts | 0 .opencode/skills/{ => Utilities}/System/Tools/UpdateIndex.ts | 0 .opencode/skills/{ => Utilities}/System/Tools/UpdateSearch.ts | 0 .../{ => Utilities}/System/Workflows/CrossRepoValidation.md | 0 .../skills/{ => Utilities}/System/Workflows/DocumentRecent.md | 0 .../skills/{ => Utilities}/System/Workflows/DocumentSession.md | 0 .opencode/skills/{ => Utilities}/System/Workflows/GitPush.md | 0 .../skills/{ => Utilities}/System/Workflows/IntegrityCheck.md | 0 .opencode/skills/{ => Utilities}/System/Workflows/PrivacyCheck.md | 0 .../skills/{ => Utilities}/System/Workflows/SecretScanning.md | 0 .../skills/{ => Utilities}/System/Workflows/WorkContextRecall.md | 0 .opencode/skills/{ => Utilities}/WriteStory/AestheticProfiles.md | 0 .opencode/skills/{ => Utilities}/WriteStory/AntiCliche.md | 0 .opencode/skills/{ => Utilities}/WriteStory/Critics.md | 0 .opencode/skills/{ => Utilities}/WriteStory/RhetoricalFigures.md | 0 .opencode/skills/{ => Utilities}/WriteStory/SKILL.md | 0 .opencode/skills/{ => Utilities}/WriteStory/StorrFramework.md | 0 .opencode/skills/{ => Utilities}/WriteStory/StoryLayers.md | 0 .opencode/skills/{ => Utilities}/WriteStory/StoryStructures.md | 0 .../skills/{ => Utilities}/WriteStory/Workflows/BuildBible.md | 0 .opencode/skills/{ => Utilities}/WriteStory/Workflows/Explore.md | 0 .../skills/{ => Utilities}/WriteStory/Workflows/Interview.md | 0 .opencode/skills/{ => Utilities}/WriteStory/Workflows/Revise.md | 0 .../skills/{ => Utilities}/WriteStory/Workflows/WriteChapter.md | 0 44 files changed, 0 insertions(+), 0 deletions(-) rename .opencode/skills/{ => Utilities}/AudioEditor/SKILL.md (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Analyze.help.md (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Analyze.ts (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Edit.help.md (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Edit.ts (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Pipeline.help.md (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Pipeline.ts (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Polish.help.md (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Polish.ts (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Transcribe.help.md (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Tools/Transcribe.ts (100%) rename .opencode/skills/{ => Utilities}/AudioEditor/Workflows/Clean.md (100%) rename .opencode/skills/{ => Utilities}/CodeReview/SKILL.md (100%) rename .opencode/skills/{ => Utilities}/OpenCodeSystem/SKILL.md (100%) rename .opencode/skills/{ => Utilities}/Sales/SKILL.md (100%) rename .opencode/skills/{ => Utilities}/Sales/Workflows/CreateNarrative.md (100%) rename .opencode/skills/{ => Utilities}/Sales/Workflows/CreateSalesPackage.md (100%) rename .opencode/skills/{ => Utilities}/Sales/Workflows/CreateVisual.md (100%) rename .opencode/skills/{ => Utilities}/System/SKILL.md (100%) rename .opencode/skills/{ => Utilities}/System/Tools/CreateUpdate.ts (100%) rename .opencode/skills/{ => Utilities}/System/Tools/SecretScan.ts (100%) rename .opencode/skills/{ => Utilities}/System/Tools/UpdateIndex.ts (100%) rename .opencode/skills/{ => Utilities}/System/Tools/UpdateSearch.ts (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/CrossRepoValidation.md (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/DocumentRecent.md (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/DocumentSession.md (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/GitPush.md (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/IntegrityCheck.md (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/PrivacyCheck.md (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/SecretScanning.md (100%) rename .opencode/skills/{ => Utilities}/System/Workflows/WorkContextRecall.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/AestheticProfiles.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/AntiCliche.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/Critics.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/RhetoricalFigures.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/SKILL.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/StorrFramework.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/StoryLayers.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/StoryStructures.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/Workflows/BuildBible.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/Workflows/Explore.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/Workflows/Interview.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/Workflows/Revise.md (100%) rename .opencode/skills/{ => Utilities}/WriteStory/Workflows/WriteChapter.md (100%) diff --git a/.opencode/skills/AudioEditor/SKILL.md b/.opencode/skills/Utilities/AudioEditor/SKILL.md similarity index 100% rename from .opencode/skills/AudioEditor/SKILL.md rename to .opencode/skills/Utilities/AudioEditor/SKILL.md diff --git a/.opencode/skills/AudioEditor/Tools/Analyze.help.md b/.opencode/skills/Utilities/AudioEditor/Tools/Analyze.help.md similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Analyze.help.md rename to .opencode/skills/Utilities/AudioEditor/Tools/Analyze.help.md diff --git a/.opencode/skills/AudioEditor/Tools/Analyze.ts b/.opencode/skills/Utilities/AudioEditor/Tools/Analyze.ts similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Analyze.ts rename to .opencode/skills/Utilities/AudioEditor/Tools/Analyze.ts diff --git a/.opencode/skills/AudioEditor/Tools/Edit.help.md b/.opencode/skills/Utilities/AudioEditor/Tools/Edit.help.md similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Edit.help.md rename to .opencode/skills/Utilities/AudioEditor/Tools/Edit.help.md diff --git a/.opencode/skills/AudioEditor/Tools/Edit.ts b/.opencode/skills/Utilities/AudioEditor/Tools/Edit.ts similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Edit.ts rename to .opencode/skills/Utilities/AudioEditor/Tools/Edit.ts diff --git a/.opencode/skills/AudioEditor/Tools/Pipeline.help.md b/.opencode/skills/Utilities/AudioEditor/Tools/Pipeline.help.md similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Pipeline.help.md rename to .opencode/skills/Utilities/AudioEditor/Tools/Pipeline.help.md diff --git a/.opencode/skills/AudioEditor/Tools/Pipeline.ts b/.opencode/skills/Utilities/AudioEditor/Tools/Pipeline.ts similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Pipeline.ts rename to .opencode/skills/Utilities/AudioEditor/Tools/Pipeline.ts diff --git a/.opencode/skills/AudioEditor/Tools/Polish.help.md b/.opencode/skills/Utilities/AudioEditor/Tools/Polish.help.md similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Polish.help.md rename to .opencode/skills/Utilities/AudioEditor/Tools/Polish.help.md diff --git a/.opencode/skills/AudioEditor/Tools/Polish.ts b/.opencode/skills/Utilities/AudioEditor/Tools/Polish.ts similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Polish.ts rename to .opencode/skills/Utilities/AudioEditor/Tools/Polish.ts diff --git a/.opencode/skills/AudioEditor/Tools/Transcribe.help.md b/.opencode/skills/Utilities/AudioEditor/Tools/Transcribe.help.md similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Transcribe.help.md rename to .opencode/skills/Utilities/AudioEditor/Tools/Transcribe.help.md diff --git a/.opencode/skills/AudioEditor/Tools/Transcribe.ts b/.opencode/skills/Utilities/AudioEditor/Tools/Transcribe.ts similarity index 100% rename from .opencode/skills/AudioEditor/Tools/Transcribe.ts rename to .opencode/skills/Utilities/AudioEditor/Tools/Transcribe.ts diff --git a/.opencode/skills/AudioEditor/Workflows/Clean.md b/.opencode/skills/Utilities/AudioEditor/Workflows/Clean.md similarity index 100% rename from .opencode/skills/AudioEditor/Workflows/Clean.md rename to .opencode/skills/Utilities/AudioEditor/Workflows/Clean.md diff --git a/.opencode/skills/CodeReview/SKILL.md b/.opencode/skills/Utilities/CodeReview/SKILL.md similarity index 100% rename from .opencode/skills/CodeReview/SKILL.md rename to .opencode/skills/Utilities/CodeReview/SKILL.md diff --git a/.opencode/skills/OpenCodeSystem/SKILL.md b/.opencode/skills/Utilities/OpenCodeSystem/SKILL.md similarity index 100% rename from .opencode/skills/OpenCodeSystem/SKILL.md rename to .opencode/skills/Utilities/OpenCodeSystem/SKILL.md diff --git a/.opencode/skills/Sales/SKILL.md b/.opencode/skills/Utilities/Sales/SKILL.md similarity index 100% rename from .opencode/skills/Sales/SKILL.md rename to .opencode/skills/Utilities/Sales/SKILL.md diff --git a/.opencode/skills/Sales/Workflows/CreateNarrative.md b/.opencode/skills/Utilities/Sales/Workflows/CreateNarrative.md similarity index 100% rename from .opencode/skills/Sales/Workflows/CreateNarrative.md rename to .opencode/skills/Utilities/Sales/Workflows/CreateNarrative.md diff --git a/.opencode/skills/Sales/Workflows/CreateSalesPackage.md b/.opencode/skills/Utilities/Sales/Workflows/CreateSalesPackage.md similarity index 100% rename from .opencode/skills/Sales/Workflows/CreateSalesPackage.md rename to .opencode/skills/Utilities/Sales/Workflows/CreateSalesPackage.md diff --git a/.opencode/skills/Sales/Workflows/CreateVisual.md b/.opencode/skills/Utilities/Sales/Workflows/CreateVisual.md similarity index 100% rename from .opencode/skills/Sales/Workflows/CreateVisual.md rename to .opencode/skills/Utilities/Sales/Workflows/CreateVisual.md diff --git a/.opencode/skills/System/SKILL.md b/.opencode/skills/Utilities/System/SKILL.md similarity index 100% rename from .opencode/skills/System/SKILL.md rename to .opencode/skills/Utilities/System/SKILL.md diff --git a/.opencode/skills/System/Tools/CreateUpdate.ts b/.opencode/skills/Utilities/System/Tools/CreateUpdate.ts similarity index 100% rename from .opencode/skills/System/Tools/CreateUpdate.ts rename to .opencode/skills/Utilities/System/Tools/CreateUpdate.ts diff --git a/.opencode/skills/System/Tools/SecretScan.ts b/.opencode/skills/Utilities/System/Tools/SecretScan.ts similarity index 100% rename from .opencode/skills/System/Tools/SecretScan.ts rename to .opencode/skills/Utilities/System/Tools/SecretScan.ts diff --git a/.opencode/skills/System/Tools/UpdateIndex.ts b/.opencode/skills/Utilities/System/Tools/UpdateIndex.ts similarity index 100% rename from .opencode/skills/System/Tools/UpdateIndex.ts rename to .opencode/skills/Utilities/System/Tools/UpdateIndex.ts diff --git a/.opencode/skills/System/Tools/UpdateSearch.ts b/.opencode/skills/Utilities/System/Tools/UpdateSearch.ts similarity index 100% rename from .opencode/skills/System/Tools/UpdateSearch.ts rename to .opencode/skills/Utilities/System/Tools/UpdateSearch.ts diff --git a/.opencode/skills/System/Workflows/CrossRepoValidation.md b/.opencode/skills/Utilities/System/Workflows/CrossRepoValidation.md similarity index 100% rename from .opencode/skills/System/Workflows/CrossRepoValidation.md rename to .opencode/skills/Utilities/System/Workflows/CrossRepoValidation.md diff --git a/.opencode/skills/System/Workflows/DocumentRecent.md b/.opencode/skills/Utilities/System/Workflows/DocumentRecent.md similarity index 100% rename from .opencode/skills/System/Workflows/DocumentRecent.md rename to .opencode/skills/Utilities/System/Workflows/DocumentRecent.md diff --git a/.opencode/skills/System/Workflows/DocumentSession.md b/.opencode/skills/Utilities/System/Workflows/DocumentSession.md similarity index 100% rename from .opencode/skills/System/Workflows/DocumentSession.md rename to .opencode/skills/Utilities/System/Workflows/DocumentSession.md diff --git a/.opencode/skills/System/Workflows/GitPush.md b/.opencode/skills/Utilities/System/Workflows/GitPush.md similarity index 100% rename from .opencode/skills/System/Workflows/GitPush.md rename to .opencode/skills/Utilities/System/Workflows/GitPush.md diff --git a/.opencode/skills/System/Workflows/IntegrityCheck.md b/.opencode/skills/Utilities/System/Workflows/IntegrityCheck.md similarity index 100% rename from .opencode/skills/System/Workflows/IntegrityCheck.md rename to .opencode/skills/Utilities/System/Workflows/IntegrityCheck.md diff --git a/.opencode/skills/System/Workflows/PrivacyCheck.md b/.opencode/skills/Utilities/System/Workflows/PrivacyCheck.md similarity index 100% rename from .opencode/skills/System/Workflows/PrivacyCheck.md rename to .opencode/skills/Utilities/System/Workflows/PrivacyCheck.md diff --git a/.opencode/skills/System/Workflows/SecretScanning.md b/.opencode/skills/Utilities/System/Workflows/SecretScanning.md similarity index 100% rename from .opencode/skills/System/Workflows/SecretScanning.md rename to .opencode/skills/Utilities/System/Workflows/SecretScanning.md diff --git a/.opencode/skills/System/Workflows/WorkContextRecall.md b/.opencode/skills/Utilities/System/Workflows/WorkContextRecall.md similarity index 100% rename from .opencode/skills/System/Workflows/WorkContextRecall.md rename to .opencode/skills/Utilities/System/Workflows/WorkContextRecall.md diff --git a/.opencode/skills/WriteStory/AestheticProfiles.md b/.opencode/skills/Utilities/WriteStory/AestheticProfiles.md similarity index 100% rename from .opencode/skills/WriteStory/AestheticProfiles.md rename to .opencode/skills/Utilities/WriteStory/AestheticProfiles.md diff --git a/.opencode/skills/WriteStory/AntiCliche.md b/.opencode/skills/Utilities/WriteStory/AntiCliche.md similarity index 100% rename from .opencode/skills/WriteStory/AntiCliche.md rename to .opencode/skills/Utilities/WriteStory/AntiCliche.md diff --git a/.opencode/skills/WriteStory/Critics.md b/.opencode/skills/Utilities/WriteStory/Critics.md similarity index 100% rename from .opencode/skills/WriteStory/Critics.md rename to .opencode/skills/Utilities/WriteStory/Critics.md diff --git a/.opencode/skills/WriteStory/RhetoricalFigures.md b/.opencode/skills/Utilities/WriteStory/RhetoricalFigures.md similarity index 100% rename from .opencode/skills/WriteStory/RhetoricalFigures.md rename to .opencode/skills/Utilities/WriteStory/RhetoricalFigures.md diff --git a/.opencode/skills/WriteStory/SKILL.md b/.opencode/skills/Utilities/WriteStory/SKILL.md similarity index 100% rename from .opencode/skills/WriteStory/SKILL.md rename to .opencode/skills/Utilities/WriteStory/SKILL.md diff --git a/.opencode/skills/WriteStory/StorrFramework.md b/.opencode/skills/Utilities/WriteStory/StorrFramework.md similarity index 100% rename from .opencode/skills/WriteStory/StorrFramework.md rename to .opencode/skills/Utilities/WriteStory/StorrFramework.md diff --git a/.opencode/skills/WriteStory/StoryLayers.md b/.opencode/skills/Utilities/WriteStory/StoryLayers.md similarity index 100% rename from .opencode/skills/WriteStory/StoryLayers.md rename to .opencode/skills/Utilities/WriteStory/StoryLayers.md diff --git a/.opencode/skills/WriteStory/StoryStructures.md b/.opencode/skills/Utilities/WriteStory/StoryStructures.md similarity index 100% rename from .opencode/skills/WriteStory/StoryStructures.md rename to .opencode/skills/Utilities/WriteStory/StoryStructures.md diff --git a/.opencode/skills/WriteStory/Workflows/BuildBible.md b/.opencode/skills/Utilities/WriteStory/Workflows/BuildBible.md similarity index 100% rename from .opencode/skills/WriteStory/Workflows/BuildBible.md rename to .opencode/skills/Utilities/WriteStory/Workflows/BuildBible.md diff --git a/.opencode/skills/WriteStory/Workflows/Explore.md b/.opencode/skills/Utilities/WriteStory/Workflows/Explore.md similarity index 100% rename from .opencode/skills/WriteStory/Workflows/Explore.md rename to .opencode/skills/Utilities/WriteStory/Workflows/Explore.md diff --git a/.opencode/skills/WriteStory/Workflows/Interview.md b/.opencode/skills/Utilities/WriteStory/Workflows/Interview.md similarity index 100% rename from .opencode/skills/WriteStory/Workflows/Interview.md rename to .opencode/skills/Utilities/WriteStory/Workflows/Interview.md diff --git a/.opencode/skills/WriteStory/Workflows/Revise.md b/.opencode/skills/Utilities/WriteStory/Workflows/Revise.md similarity index 100% rename from .opencode/skills/WriteStory/Workflows/Revise.md rename to .opencode/skills/Utilities/WriteStory/Workflows/Revise.md diff --git a/.opencode/skills/WriteStory/Workflows/WriteChapter.md b/.opencode/skills/Utilities/WriteStory/Workflows/WriteChapter.md similarity index 100% rename from .opencode/skills/WriteStory/Workflows/WriteChapter.md rename to .opencode/skills/Utilities/WriteStory/Workflows/WriteChapter.md From 96426d47c8bf651081410988c4b3d35760a514ce Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:40:26 -0400 Subject: [PATCH 2/8] feat: add missing PAI/ root docs from 4.0.3 Added 3 files missing from PAI/ root: - doc-dependencies.json (documentation dependency graph) - PIPELINES.md (pipeline system documentation) - THEHOOKSYSTEM.md (hooks/plugin system documentation) All .claude/ references replaced with .opencode/. --- .opencode/PAI/PIPELINES.md | 270 ++++++ .opencode/PAI/THEHOOKSYSTEM.md | 1327 +++++++++++++++++++++++++++ .opencode/PAI/doc-dependencies.json | 178 ++++ 3 files changed, 1775 insertions(+) create mode 100755 .opencode/PAI/PIPELINES.md create mode 100755 .opencode/PAI/THEHOOKSYSTEM.md create mode 100644 .opencode/PAI/doc-dependencies.json diff --git a/.opencode/PAI/PIPELINES.md b/.opencode/PAI/PIPELINES.md new file mode 100755 index 00000000..d21acc3d --- /dev/null +++ b/.opencode/PAI/PIPELINES.md @@ -0,0 +1,270 @@ +# Pipelines + +> **PAI 4.0** — This system is under active development. APIs, configuration formats, and features may change without notice. + +**Chaining Actions into Sequential Workflows** + +Pipelines are the fourth primitive in the architecture. They chain Actions together into multi-step workflows using the pipe model. + +> **Note:** Personal pipeline definitions are stored in `USER/PIPELINES/`. This document describes the framework. + +--- + +## What Pipelines Are + +Pipelines orchestrate **sequences of Actions** into cohesive workflows. They differ from Actions in a critical way: Actions are single-step workflow patterns, while Pipelines chain multiple Actions together in sequence using the pipe model (output of action N becomes input of action N+1). + +**The Pipeline Pattern:** + +``` +Input → Action1 → Action2 → Action3 → Output + (each action receives upstream output via passthrough) +``` + +**Real Example - Content Processing:** + +``` +RSS Item → A_PARSE → A_ENRICH → A_FORMAT → A_SEND_EMAIL → Delivered +``` + +**When Actions Run Alone vs In Pipelines:** + +| Scenario | Use | +|----------|-----| +| Single task with clear input/output | **Action** | +| Multi-step workflow with dependencies | **Pipeline** | +| Parallel independent tasks | Multiple **Actions** | +| Sequential dependent tasks | **Pipeline** | + +--- + +## Pipe Model + +Pipelines use a Unix-style pipe model: the output of action N becomes the input of action N+1. + +``` +Input → Action1 → Action2 → Action3 → Output + | | | + transform enrich format +``` + +### Passthrough Pattern + +Actions use the passthrough pattern (`...upstream`) to preserve metadata from previous actions while adding their own output. This ensures that context accumulates as data moves through the pipeline rather than being discarded at each step. + +```typescript +// Action receives upstream data, adds its own, passes everything forward +return { + ...upstream, // preserve all prior action output + myField: result, // add this action's contribution +}; +``` + +The final action in a pipeline has access to every field produced by every preceding action --- not just the immediately previous one. + +--- + +## Pipeline Definition + +### YAML Format (Arbol) + +In the Arbol system, pipelines are defined as YAML files that declare an ordered list of actions: + +```yaml +name: P_MY_PIPELINE +description: Processes items through enrichment and formatting +actions: + - A_PARSE + - A_ENRICH + - A_FORMAT +``` + +The pipeline worker calls each action in sequence via service bindings, passing output forward via the pipe model. + +### PIPELINE.md Format (Local) + +Local pipeline definitions live in `~/.opencode/PAI/PIPELINES/[Domain]_[Pipeline-Name]/PIPELINE.md` + +```markdown +# [Pipeline_Name] Pipeline + +**Purpose:** [One sentence describing what this pipeline achieves] +**Domain:** [e.g., Blog, Newsletter, Art, PAI] +**Version:** 1.0 + +--- + +## Pipeline Overview + +| Step | Action | Purpose | +|------|--------|---------| +| 1 | [Action_Name] | [What this step accomplishes] | +| 2 | [Action_Name] | [What this step accomplishes] | +| 3 | [Action_Name] | [What this step accomplishes] | +``` + +### Naming Convention + +``` +~/.opencode/PAI/PIPELINES/ +├── Blog_Publish-Post/ # Domain_Action-Format +│ └── PIPELINE.md +├── Newsletter_Full-Cycle/ +│ └── PIPELINE.md +└── PIPELINE-TEMPLATE.md # Template for new pipelines (planned) +``` + +--- + +## Pipeline vs Action + +### When to Use an Action + +- Single discrete task +- Clear input/output contract +- No dependencies on other Actions +- Can run in isolation + +**Examples:** `Blog_Deploy`, `Art_Create-Essay-Header`, `Newsletter_Send` + +### When to Use a Pipeline + +- Multiple dependent steps +- Sequential processing with data accumulation +- Complex workflow with ordered operations + +**Examples:** `Blog_Publish-Post`, `Newsletter_Full-Cycle`, `PAI_Release` + +### Decision Matrix + +| Criteria | Action | Pipeline | +|----------|--------|----------| +| Steps | 1 | 2+ | +| Dependencies | None | Sequential | +| Data model | Single input/output | Passthrough accumulation | +| Reusability | High (composable) | Orchestration layer | + +--- + +## Creating New Pipelines + +### Step 1: Identify the Workflow + +Map out the complete workflow: + +1. What Actions already exist that can be chained? +2. What new Actions need to be created? +3. What data needs to pass between steps via the passthrough pattern? + +### Step 2: Create Pipeline Directory + +```bash +mkdir -p ~/.opencode/PAI/PIPELINES/[Domain]_[Pipeline-Name] +# PIPELINE-TEMPLATE.md is planned but not yet created +# For now, copy an existing pipeline and modify it +cp ~/.opencode/PAI/PIPELINES/Blog_Publish-Post/PIPELINE.md ~/.opencode/PAI/PIPELINES/[Domain]_[Pipeline-Name]/PIPELINE.md +``` + +### Step 3: Define Overview Table + +```markdown +## Pipeline Overview + +| Step | Action | Purpose | +|------|--------|---------| +| 1 | Action_One | First step purpose | +| 2 | Action_Two | Second step purpose | +| 3 | Action_Three | Third step purpose | +``` + +### Step 4: Define Each Step + +For each step, specify: + +1. **Action** - Path to ACTION.md or Arbol action name (A_NAME) +2. **Input** - What this step requires (from upstream passthrough or initial input) +3. **Output** - What fields this step adds to the passthrough object + +--- + +## Pipeline Execution Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ PIPELINE START │ +│ (receives input) │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Action 1: Execute │ +│ └─► Receives input, returns { ...upstream, ownFields } │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Action 2: Execute │ +│ └─► Receives Action 1 output, adds its own fields │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ + [Repeat for each action] + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Final Action: Execute │ +│ └─► Has access to ALL upstream fields │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ PIPELINE COMPLETE │ +│ (returns accumulated output) │ +└─────────────────────────────────────────────────────────┘ +``` + +**Note on iteration:** Pipelines always run once. If iteration is needed, the calling Flow handles it via the Loop Gate pattern (see `FLOWS.md`). + +--- + +## Best Practices + +### 1. Keep Steps Atomic + +Each step should do one thing. If a step is doing multiple things, split into multiple steps. + +### 2. Use the Passthrough Pattern + +Always spread upstream data (`...upstream`) so downstream actions have access to all prior fields. Never discard upstream context. + +### 3. Document Data Flow + +For each action in the pipeline, document what fields it reads from upstream and what fields it adds. This makes the data contract explicit. + +### 4. Keep Actions Reusable + +Actions should not be tightly coupled to a specific pipeline. Design them to work with any upstream data shape via the passthrough pattern. + +--- + +## Related Documentation + +- **Actions:** `~/.opencode/PAI/ACTIONS.md` +- **Flows:** `~/.opencode/PAI/FLOWS.md` +- **Architecture:** `~/.opencode/PAI/PAISYSTEMARCHITECTURE.md` +- **Detailed README:** `~/.opencode/PAI/PIPELINES/README.md` +- **Source code:** `~/Projects/arbol/` + +--- + +**Last Updated:** 2026-02-22 + +--- + +## Changelog + +| Date | Change | Author | Related | +|------|--------|--------|---------| +| 2026-02-22 | Removed unimplemented verification gate system, added pipe model docs, aligned with actual Arbol codebase | {DAIDENTITY.NAME} | ARBOLSYSTEM.md, FLOWS.md | +| 2026-02-03 | Updated cross-references to new ACTIONS.md and FLOWS.md | {DAIDENTITY.NAME} | ACTIONS.md, FLOWS.md | +| 2026-01-01 | Initial document creation | {DAIDENTITY.NAME} | - | diff --git a/.opencode/PAI/THEHOOKSYSTEM.md b/.opencode/PAI/THEHOOKSYSTEM.md new file mode 100755 index 00000000..b46c2bc9 --- /dev/null +++ b/.opencode/PAI/THEHOOKSYSTEM.md @@ -0,0 +1,1327 @@ +# Hook System + +> **PAI 4.0** — This system is under active development. APIs, configuration formats, and features may change without notice. + +**Event-Driven Automation Infrastructure** + +**Location:** `~/.opencode/hooks/` +**Configuration:** `~/.opencode/settings.json` +**Status:** Active - 20 hooks running in production + +--- + +## Overview + +The PAI hook system is an event-driven automation infrastructure built on Claude Code's native hook support. Hooks are executable scripts (TypeScript/Python) that run automatically in response to specific events during Claude Code sessions. + +**Core Capabilities:** +- **Session Management** - Auto-load context, capture summaries, manage state +- **Voice Notifications** - Text-to-speech announcements for task completions +- **History Capture** - Automatic work/learning documentation to `~/.opencode/MEMORY/` +- **Multi-Agent Support** - Agent-specific hooks with voice routing +- **Tab Titles** - Dynamic terminal tab updates with task context +- **Unified Event Stream** - All hooks emit structured events to `events.jsonl` for real-time observability + +**Key Principle:** Hooks run asynchronously and fail gracefully. They enhance the user experience but never block Claude Code's core functionality. + +--- + +## Available Hook Types + +Claude Code supports the following hook events: + +### 1. **SessionStart** +**When:** Claude Code session begins (new conversation) +**Use Cases:** +- Load PAI context from `PAI/SKILL.md` +- Initialize session state +- Capture session metadata + +**Current Hooks:** +```json +{ + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${PAI_DIR}/hooks/KittyEnvPersist.hook.ts" + }, + { + "type": "command", + "command": "${PAI_DIR}/hooks/LoadContext.hook.ts" + } + ] + } + ] +} +``` + +**What They Do:** +- `KittyEnvPersist.hook.ts` - Persists Kitty terminal env vars to disk and resets tab title to clean state +- `LoadContext.hook.ts` - Injects dynamic context (relationship, learning, work summary) as `` at session start + +--- + +### 2. **SessionEnd** +**When:** Claude Code session terminates (conversation ends) +**Use Cases:** +- Capture work completions and learning moments +- Generate session summaries +- Record relationship context +- Update system counts (skills, hooks, signals) +- Run integrity checks + +**Current Hooks:** +```json +{ + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "${PAI_DIR}/hooks/WorkCompletionLearning.hook.ts" + }, + { + "type": "command", + "command": "${PAI_DIR}/hooks/SessionCleanup.hook.ts" + }, + { + "type": "command", + "command": "${PAI_DIR}/hooks/RelationshipMemory.hook.ts" + }, + { + "type": "command", + "command": "${PAI_DIR}/hooks/UpdateCounts.hook.ts" + }, + { + "type": "command", + "command": "${PAI_DIR}/hooks/IntegrityCheck.hook.ts" + } + ] + } + ] +} +``` + +**What They Do:** +- `WorkCompletionLearning.hook.ts` - Reads PRD.md frontmatter for work metadata and ISC section for criteria status, captures learning to `MEMORY/LEARNING/` for significant work sessions +- `SessionCleanup.hook.ts` - Marks PRD.md frontmatter status→COMPLETED and sets completed_at timestamp, clears session state, resets tab, cleans session names +- `RelationshipMemory.hook.ts` - Captures relationship context (observations, behaviors) to `MEMORY/RELATIONSHIP/` +- `UpdateCounts.hook.ts` - Updates system counts (skills, hooks, signals, workflows, files) displayed in the startup banner +- `IntegrityCheck.hook.ts` - Runs DocCrossRefIntegrity and SystemIntegrity checks at session end + +--- + +### 3. **UserPromptSubmit** +**When:** User submits a new prompt to Claude +**Use Cases:** +- Update UI indicators +- Pre-process user input +- Capture prompts for analysis +- Detect ratings and sentiment + +**Current Hooks:** +```json +{ + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "${PAI_DIR}/hooks/RatingCapture.hook.ts" + }, + { + "type": "command", + "command": "${PAI_DIR}/hooks/UpdateTabTitle.hook.ts" + }, + { + "type": "command", + "command": "${PAI_DIR}/hooks/SessionAutoName.hook.ts" + } + ] + } + ] +} +``` + +**What They Do:** + +**RatingCapture.hook.ts** - Unified Rating Detection +- Handles both explicit ratings ("7", "8 - good work") and implicit sentiment analysis +- Explicit path: Pattern match first (no inference needed), writes to `ratings.jsonl` +- Implicit path: Haiku inference for sentiment if no explicit match +- Low ratings (<6) auto-capture as learning opportunities +- Writes to `~/.opencode/MEMORY/SIGNALS/ratings.jsonl` +- Uses shared libraries: `hooks/lib/learning-utils.ts`, `hooks/lib/time.ts` +- **Inference:** `import { inference } from '../PAI/Tools/Inference'` → `inference({ level: 'fast', expectJson: true })` + +**UpdateTabTitle.hook.ts** - Tab Title + Working State +- Updates Kitty terminal tab title with task summary + `…` suffix +- Sets tab to **orange background** (working state) +- Announces via voice server with context-appropriate gerund +- See `TERMINALTABS.md` for full state system documentation +- **Inference:** `import { inference } from '../PAI/Tools/Inference'` → `inference({ level: 'fast' })` + +**SessionAutoName.hook.ts** - Automatic Session Naming +- Infers a short descriptive name for the session from the first substantive prompt +- Updates `MEMORY/STATE/session-names.json` with the session ID → name mapping +- Used by the startup banner and session management tools +- **Inference:** `import { inference } from '../PAI/Tools/Inference'` → `inference({ level: 'fast' })` + +--- + +### 4. **Stop** +**When:** Main agent ({DAIDENTITY.NAME}) completes a response +**Use Cases:** +- Voice notifications for task completion +- Capture work summaries and learnings +- **Update terminal tab with final state** (color + suffix based on outcome) + +**Current Hooks:** +```json +{ + "Stop": [ + { + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/LastResponseCache.hook.ts" }, + { "type": "command", "command": "${PAI_DIR}/hooks/ResponseTabReset.hook.ts" }, + { "type": "command", "command": "${PAI_DIR}/hooks/VoiceCompletion.hook.ts" }, + { "type": "command", "command": "${PAI_DIR}/hooks/DocIntegrity.hook.ts" }, + { "type": "command", "command": "${PAI_DIR}/hooks/AlgorithmTab.hook.ts" } + ] + } + ] +} +``` + +**What They Do:** + +Each Stop hook is a self-contained `.hook.ts` file that reads stdin via shared `hooks/lib/hook-io.ts`, calls its handler, and exits. Handlers in `hooks/handlers/` are unchanged — each hook is a thin wrapper. + +**`LastResponseCache.hook.ts`** — Cache last response for RatingCapture bridge +- Writes `last_assistant_message` (or transcript fallback) to `MEMORY/STATE/last-response.txt` +- RatingCapture reads this on the next UserPromptSubmit to access the previous response + +**`ResponseTabReset.hook.ts`** — Reset Kitty tab title/color after response +- Calls `handlers/TabState.ts` to set completed state +- Converts working gerund title to past tense + +**`VoiceCompletion.hook.ts`** — Send 🗣️ voice line to TTS server +- Calls `handlers/VoiceNotification.ts` for voice delivery +- Voice gate: only main sessions (checks `kitty-sessions/{sessionId}.json`) +- Subagents have no kitty-sessions file → voice blocked + +**`AlgorithmTab.hook.ts`** — Show Algorithm phase + progress in Kitty tab title +- Reads `work.json`, finds most recently updated active session, sets tab title + +**`DocIntegrity.hook.ts`** — Cross-reference + semantic drift checks +- Calls `handlers/DocCrossRefIntegrity.ts` — deterministic + inference-powered doc updates +- Self-gating: returns instantly when no system files were modified + +**Tab State System:** See `TERMINALTABS.md` for complete documentation + +--- + +### 5. **PreToolUse** +**When:** Before Claude executes any tool +**Use Cases:** +- Voice curl gating (prevent background agents from speaking) +- Security validation across file operations (Bash, Edit, Write, Read) +- Tab state updates on questions +- Agent execution guardrails +- Skill invocation validation + +**Current Hooks:** +```json +{ + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } + ] + }, + { + "matcher": "Edit", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } + ] + }, + { + "matcher": "Write", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } + ] + }, + { + "matcher": "Read", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } + ] + }, + { + "matcher": "AskUserQuestion", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/SetQuestionTab.hook.ts" } + ] + }, + { + "matcher": "Task", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/AgentExecutionGuard.hook.ts" } + ] + }, + { + "matcher": "Skill", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/SkillGuard.hook.ts" } + ] + } + ] +} +``` + +**What They Do:** +- `SecurityValidator.hook.ts` - Validates operations against security patterns. Runs on **4 matchers**: Bash (dangerous commands), Edit (sensitive file protection), Write (sensitive file protection), Read (sensitive path access) +- `SetQuestionTab.hook.ts` - Updates tab state to "awaiting input" when AskUserQuestion is invoked +- `AgentExecutionGuard.hook.ts` - Validates agent spawning (Task tool) against execution policies +- `SkillGuard.hook.ts` - Prevents false skill invocations (e.g., blocks keybindings-help unless explicitly requested) + +--- + +### 6. **PostToolUse** +**When:** After Claude executes any tool +**Status:** Active - Algorithm state tracking + +**Current Hooks:** +```json +{ + "PostToolUse": [ + { + "matcher": "AskUserQuestion", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/QuestionAnswered.hook.ts" } + ] + }, + { + "matcher": "Write", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/PRDSync.hook.ts" } + ] + }, + { + "matcher": "Edit", + "hooks": [ + { "type": "command", "command": "${PAI_DIR}/hooks/PRDSync.hook.ts" } + ] + } + ] +} +``` + +**What They Do:** + +**QuestionAnswered.hook.ts** - Post-Question Processing +- Fires after AskUserQuestion completes (user has answered) +- Captures the question and answer for session context +- Used for analytics and learning from user preferences + +**PRDSync.hook.ts** - PRD Frontmatter → work.json Sync +- Fires after Write/Edit to PRD files in `MEMORY/WORK/` +- Syncs PRD frontmatter (status, title, effort) to `MEMORY/STATE/work.json` +- Keeps work registry in sync without manual updates +- Non-blocking, fire-and-forget + +--- + +### 7. **PreCompact** +**When:** Before Claude compacts context (long conversations) +**Status:** Not currently configured + +**Potential Use Cases:** +- Preserve important context before compaction +- Log compaction events + +--- + +## Configuration + +### Location +**File:** `~/.opencode/settings.json` +**Section:** `"hooks": { ... }` + +### Environment Variables +Hooks have access to all environment variables from `~/.opencode/settings.json` `"env"` section: + +```json +{ + "env": { + "PAI_DIR": "$HOME/.claude", + "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000" + } +} +``` + +**Key Variables:** +- `PAI_DIR` - PAI installation directory (typically `~/.claude`) +- Hook scripts reference `${PAI_DIR}` in command paths + +### Identity Configuration (Central to Install Wizard) + +**settings.json is the single source of truth for all daidentity/configuration.** + +```json +{ + "daidentity": { + "name": "PAI", + "fullName": "Personal AI", + "displayName": "PAI", + "color": "#3B82F6", + "voiceId": "{YourElevenLabsVoiceId}" + }, + "principal": { + "name": "{YourName}", + "pronunciation": "{YourName}", + "timezone": "America/Los_Angeles" + } +} +``` + +**Using the Identity Module:** +```typescript +import { getIdentity, getPrincipal, getDAName, getPrincipalName, getVoiceId } from './lib/identity'; + +// Get full identity objects +const identity = getIdentity(); // { name, fullName, displayName, voiceId, color } +const principal = getPrincipal(); // { name, pronunciation, timezone } + +// Convenience functions +const DA_NAME = getDAName(); // "PAI" +const USER_NAME = getPrincipalName(); // "{YourName}" +const VOICE_ID = getVoiceId(); // from settings.json daidentity.voiceId +``` + +**Why settings.json?** +- Programmatic access via `JSON.parse()` - no regex parsing markdown +- Central to the PAI install wizard +- Single source of truth for all configuration +- Tool-friendly: easy to read/write from any language + +### Hook Configuration Structure + +```json +{ + "hooks": { + "HookEventName": [ + { + "matcher": "pattern", // Optional: filter which tools/events trigger hook + "hooks": [ + { + "type": "command", + "command": "${PAI_DIR}/hooks/my-hook.ts --arg value" + } + ] + } + ] + } +} +``` + +**Fields:** +- `HookEventName` - One of: SessionStart, SessionEnd, UserPromptSubmit, Stop, PreToolUse, PostToolUse, PreCompact +- `matcher` - Pattern to match (use `"*"` for all tools, or specific tool names) +- `type` - Always `"command"` (executes external script) +- `command` - Path to executable hook script (TypeScript/Python/Bash) + +### Hook Input (stdin) +All hooks receive JSON data on stdin: + +```typescript +{ + session_id: string; // Unique session identifier + transcript_path: string; // Path to JSONL transcript + hook_event_name: string; // Event that triggered hook + prompt?: string; // User prompt (UserPromptSubmit only) + tool_name?: string; // Tool name (PreToolUse/PostToolUse) + tool_input?: any; // Tool parameters (PreToolUse) + tool_output?: any; // Tool result (PostToolUse) + // ... event-specific fields +} +``` + +--- + +## Common Patterns + +### 1. Voice Notifications + +**Pattern:** Extract completion message → Send to voice server + +```typescript +// handlers/VoiceNotification.ts pattern +import { getIdentity } from './lib/identity'; + +const identity = getIdentity(); +const completionMessage = extractCompletionMessage(lastMessage); + +const payload = { + title: identity.name, + message: completionMessage, + voice_enabled: true, + voice_id: identity.voiceId // From settings.json +}; + +await fetch('http://localhost:8888/notify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) +}); +``` + +**Agent-Specific Voices:** +Configure voice IDs via `settings.json` daidentity section or environment variables. +Each agent can have a unique ElevenLabs voice configured. See the Agents skill for voice registry. + +--- + +### 2. History Capture (UOCS Pattern) + +**Pattern:** Parse structured response → Save to appropriate history directory + +**File Naming Convention:** +``` +YYYY-MM-DD-HHMMSS_TYPE_description.md +``` + +**Types:** +- `WORK` - General task completions +- `LEARNING` - Problem-solving learnings +- `SESSION` - Session summaries +- `RESEARCH` - Research findings (from agents) +- `FEATURE` - Feature implementations (from agents) +- `DECISION` - Architectural decisions (from agents) + +**Example pattern (from WorkCompletionLearning.hook.ts):** +```typescript +import { getLearningCategory, isLearningCapture } from './lib/learning-utils'; +import { getPSTTimestamp, getYearMonth } from './lib/time'; + +const structured = extractStructuredSections(lastMessage); +const isLearning = isLearningCapture(text, structured.summary, structured.analysis); + +// If learning content detected, capture to LEARNING/ +if (isLearning) { + const category = getLearningCategory(text); // 'SYSTEM' or 'ALGORITHM' + const targetDir = join(baseDir, 'MEMORY', 'LEARNING', category, getYearMonth()); + const filename = generateFilename(description, 'LEARNING'); + writeFileSync(join(targetDir, filename), content); +} +``` + +**Structured Sections Parsed:** +- `📋 SUMMARY:` - Brief overview +- `🔍 ANALYSIS:` - Key findings +- `⚡ ACTIONS:` - Steps taken +- `✅ RESULTS:` - Outcomes +- `📊 STATUS:` - Current state +- `➡️ NEXT:` - Follow-up actions +- `🎯 COMPLETED:` - **Voice notification line** + +--- + +### 3. Agent Type Detection + +**Pattern:** Identify which agent is executing → Route appropriately + +```typescript +// Agent detection pattern +let agentName = getAgentForSession(sessionId); + +// Detect from Task tool +if (hookData.tool_name === 'Task' && hookData.tool_input?.subagent_type) { + agentName = hookData.tool_input.subagent_type; + setAgentForSession(sessionId, agentName); +} + +// Detect from CLAUDE_CODE_AGENT env variable +else if (process.env.CLAUDE_CODE_AGENT) { + agentName = process.env.CLAUDE_CODE_AGENT; +} + +// Detect from path (subagents run in /agents/name/) +else if (hookData.cwd && hookData.cwd.includes('/agents/')) { + const agentMatch = hookData.cwd.match(/\/agents\/([^\/]+)/); + if (agentMatch) agentName = agentMatch[1]; +} +``` + +**Session Mapping:** `~/.opencode/MEMORY/STATE/agent-sessions.json` +```json +{ + "session-id-abc123": "engineer", + "session-id-def456": "researcher" +} +``` + +--- + +### 4. Tab Title + Color State Architecture + +**Pattern:** Visual state feedback through tab colors and title suffixes + +**State Flow:** + +| Event | Hook | Tab Title | Inactive Color | State | +|-------|------|-----------|----------------|-------| +| UserPromptSubmit | `UpdateTabTitle.hook.ts` | `⚙️ Summary…` | Orange `#B35A00` | Working | +| Inference | `UpdateTabTitle.hook.ts` | `🧠 Analyzing…` | Orange `#B35A00` | Inference | +| Stop (success) | `handlers/TabState.ts` | `Summary` | Green `#022800` | Completed | +| Stop (question) | `handlers/TabState.ts` | `Summary?` | Teal `#0D4F4F` | Awaiting Input | +| Stop (error) | `handlers/TabState.ts` | `Summary!` | Orange `#B35A00` | Error | + +**Active Tab:** Always Dark Blue `#002B80` (state colors only affect inactive tabs) + +**Why This Design:** +- **Instant visual feedback** - See state at a glance without reading +- **Color-coded priority** - Teal tabs need attention, green tabs are done +- **Suffix as state indicator** - Works even in narrow tab bars +- **Haiku only on user input** - One AI call per prompt (not per tool) + +**State Detection (in Stop hook):** +1. Check transcript for `AskUserQuestion` tool → `awaitingInput` +2. Check `📊 STATUS:` for error patterns → `error` +3. Default → `completed` + +**Text Colors:** +- Active tab: White `#FFFFFF` (always) +- Inactive tab: Gray `#A0A0A0` (always) + +**Active Tab Background:** Dark Blue `#002B80` (always - state colors only affect inactive tabs) + +**Tab Icons:** +- 🧠 Brain - AI inference in progress (Haiku/Sonnet thinking) +- ⚙️ Gear - Processing/working state + +**Full Documentation:** See `~/.opencode/PAI/TERMINALTABS.md` + +--- + +### 5. Async Non-Blocking Execution + +**Pattern:** Hook executes quickly → Launch background processes for slow operations + +```typescript +// update-tab-titles.ts pattern +// Set immediate tab title (fast) +execSync(`printf '\\033]0;${titleWithEmoji}\\007' >&2`); + +// Launch background process for Haiku summary (slow) +Bun.spawn(['bun', `${paiDir}/hooks/UpdateTabTitle.ts`, prompt], { + stdout: 'ignore', + stderr: 'ignore', + stdin: 'ignore' +}); + +process.exit(0); // Exit immediately +``` + +**Key Principle:** Hooks must never block Claude Code. Always exit quickly, use background processes for slow work. + +--- + +### 6. Graceful Failure + +**Pattern:** Wrap everything in try/catch → Log errors → Exit successfully + +```typescript +async function main() { + try { + // Hook logic here + } catch (error) { + // Log but don't fail + console.error('Hook error:', error); + } + + process.exit(0); // Always exit 0 +} +``` + +**Why:** If hooks crash, Claude Code may freeze. Always exit cleanly. + +--- + +## Creating Custom Hooks + +### Step 1: Choose Hook Event +Decide which event should trigger your hook (SessionStart, Stop, PostToolUse, etc.) + +### Step 2: Create Hook Script +**Location:** `~/.opencode/hooks/my-custom-hook.ts` + +**Template:** +```typescript +#!/usr/bin/env bun + +interface HookInput { + session_id: string; + transcript_path: string; + hook_event_name: string; + // ... event-specific fields +} + +async function main() { + try { + // Read stdin + const input = await Bun.stdin.text(); + const data: HookInput = JSON.parse(input); + + // Your hook logic here + console.log(`Hook triggered: ${data.hook_event_name}`); + + // Example: Read transcript + const fs = require('fs'); + const transcript = fs.readFileSync(data.transcript_path, 'utf-8'); + + // Do something with the data + + } catch (error) { + // Log but don't fail + console.error('Hook error:', error); + } + + process.exit(0); // Always exit 0 +} + +main(); +``` + +### Step 3: Make Executable +```bash +chmod +x ~/.opencode/hooks/my-custom-hook.ts +``` + +### Step 4: Add to settings.json +```json +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "${PAI_DIR}/hooks/my-custom-hook.ts" + } + ] + } + ] + } +} +``` + +### Step 5: Test +```bash +# Test hook directly +echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ~/.opencode/hooks/my-custom-hook.ts +``` + +### Step 6: Restart Claude Code +Hooks are loaded at startup. Restart to apply changes. + +--- + +## Hook Development Best Practices + +### 1. **Fast Execution** +- Hooks should complete in < 500ms +- Use background processes for slow work (Haiku API calls, file processing) +- Exit immediately after launching background work + +### 2. **Graceful Failure** +- Always wrap in try/catch +- Log errors to stderr (available in hook debug logs) +- Always `process.exit(0)` - never throw or exit(1) + +### 3. **Non-Blocking** +- Never wait for external services (unless they respond quickly) +- Use `.catch(() => {})` for async operations +- Fail silently if optional services are offline + +### 4. **Stdin Reading** +- Use timeout when reading stdin (Claude Code may not send data immediately) +- Handle empty/invalid input gracefully + +```typescript +const decoder = new TextDecoder(); +const reader = Bun.stdin.stream().getReader(); + +const timeoutPromise = new Promise((resolve) => { + setTimeout(() => resolve(), 500); // 500ms timeout +}); + +await Promise.race([readPromise, timeoutPromise]); +``` + +### 5. **File I/O** +- Check `existsSync()` before reading files +- Create directories with `{ recursive: true }` +- Use PST timestamps for consistency + +### 6. **Environment Access** +- All `settings.json` env vars available via `process.env` +- Use `${PAI_DIR}` in settings.json for portability +- Access in code via `process.env.PAI_DIR` + +### 7. **Logging** +- Log useful debug info to stderr for troubleshooting +- Include relevant metadata (session_id, tool_name, etc.) +- Never log sensitive data (API keys, user content) + +--- + +## Troubleshooting + +### Hook Not Running + +**Check:** +1. Is hook script executable? `chmod +x ~/.opencode/hooks/my-hook.ts` +2. Is path correct in settings.json? Use `${PAI_DIR}/hooks/...` +3. Is settings.json valid JSON? `jq . ~/.opencode/settings.json` +4. Did you restart Claude Code after editing settings.json? + +**Debug:** +```bash +# Test hook directly +echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ~/.opencode/hooks/my-hook.ts + +# Check hook logs (stderr output) +tail -f ~/.opencode/hooks/debug.log # If you add logging +``` + +--- + +### Hook Hangs/Freezes Claude Code + +**Cause:** Hook not exiting (infinite loop, waiting for input, blocking operation) + +**Fix:** +1. Add timeouts to all blocking operations +2. Ensure `process.exit(0)` is always reached +3. Use background processes for long operations +4. Check stdin reading has timeout + +**Prevention:** +```typescript +// Always use timeout +setTimeout(() => { + console.error('Hook timeout - exiting'); + process.exit(0); +}, 5000); // 5 second max +``` + +--- + +### Voice Notifications Not Working + +**Check:** +1. Is voice server running? `curl http://localhost:8888/health` +2. Is voice_id correct? See `PAI/SKILL.md` for mappings +3. Is message format correct? `{"message":"...", "voice_id":"...", "title":"..."}` +4. Is ElevenLabs API key in `${PAI_DIR}/.env`? + +**Debug:** +```bash +# Test voice server directly +curl -X POST http://localhost:8888/notify \ + -H "Content-Type: application/json" \ + -d '{"message":"Test message","voice_id":"[YOUR_VOICE_ID]","title":"Test"}' +``` + +**Common Issues:** +- Wrong voice_id → Silent failure (invalid ID) +- Voice server offline → Hook continues (graceful failure) +- No `🎯 COMPLETED:` line → No voice notification extracted + +--- + +### Work Not Capturing + +**Check:** +1. Does `~/.opencode/MEMORY/` directory exist? +2. Does current-work file exist? Check `~/.opencode/MEMORY/STATE/current-work.json` +3. Is hook actually running? Check `~/.opencode/MEMORY/RAW/` for events +4. File permissions? `ls -la ~/.opencode/MEMORY/WORK/` + +**Debug:** +```bash +# Check current work +cat ~/.opencode/MEMORY/STATE/current-work.json + +# Check recent work directories +ls -lt ~/.opencode/MEMORY/WORK/ | head -10 +ls -lt ~/.opencode/MEMORY/LEARNING/$(date +%Y-%m)/ | head -10 + +# Check raw events +tail ~/.opencode/MEMORY/RAW/$(date +%Y-%m)/$(date +%Y-%m-%d)_all-events.jsonl +``` + +**Common Issues:** +- Missing current-work.json → Work not being tracked for this session +- Work not updating → capture handler not finding current work +- Learning detection too strict → Adjust `isLearningCapture()` logic + +--- + +### Stop Event Not Firing (RESOLVED) + +**Original Issue:** Stop events were not firing consistently in earlier Claude Code versions, causing voice notifications and work capture to fail silently. + +**Resolution:** Fixed in Claude Code updates. The Stop hooks now fires reliably. The unified orchestrator pattern (`Stop hooks.hook.ts` delegating to `handlers/`) was implemented in part to work around this — and remains the production architecture. + +**Status:** RESOLVED — Stop events now fire reliably. Stop hooks handles all post-response work. + +--- + +### Agent Detection Failing + +**Check:** +1. Is `~/.opencode/MEMORY/STATE/agent-sessions.json` writable? +2. Is `[AGENT:type]` tag in `🎯 COMPLETED:` line? +3. Is agent running from correct directory? (`/agents/name/`) + +**Debug:** +```bash +# Check session mappings +cat ~/.opencode/MEMORY/STATE/agent-sessions.json | jq . + +# Check subagent-stop debug log +tail -f ~/.opencode/hooks/subagent-stop-debug.log +``` + +**Fix:** +- Ensure agents include `[AGENT:type]` in completion line +- Verify Task tool passes `subagent_type` parameter +- Check cwd includes `/agents/` in path + +--- + +### Transcript Type Mismatch (Fixed 2026-01-11) + +**Symptom:** Context reading functions return empty results even though transcript has data + +**Root Cause:** Claude Code transcripts use `type: "user"` but hooks were checking for `type: "human"`. + +**Affected Hooks:** +- `UpdateTabTitle.hook.ts` - Couldn't read user messages for context +- `RatingCapture.hook.ts` - Same issue + +**Fix Applied:** +1. Changed `entry.type === 'human'` → `entry.type === 'user'` +2. Improved content extraction to skip `tool_result` blocks and only capture actual text + +**Verification:** +```bash +# Check transcript type field +grep '"type":"user"' ~/.opencode/projects/-Users-username--claude/*.jsonl | head -1 | jq '.type' +# Should output: "user" (not "human") +``` + +**Prevention:** When parsing transcripts, always verify the actual JSON structure first. + +--- + +### Context Loading Issues (SessionStart) + +**Check:** +1. Does `~/.opencode/PAI/SKILL.md` exist? +2. Is `LoadContext.hook.ts` executable? +3. Is `PAI_DIR` env variable set correctly? + +**Debug:** +```bash +# Test context loading directly +bun ~/.opencode/hooks/LoadContext.hook.ts + +# Should output with SKILL.md content +``` + +**Common Issues:** +- Subagent sessions loading main context → Fixed (subagent detection in hook) +- File not found → Check `PAI_DIR` environment variable +- Permission denied → `chmod +x ~/.opencode/hooks/LoadContext.hook.ts` + +--- + +## Advanced Topics + +### Multi-Hook Execution Order + +Hooks in same event execute **sequentially** in order defined in settings.json: + +```json +{ + "Stop": [ + { + "hooks": [ + { "command": "${PAI_DIR}/hooks/Stop hooks.hook.ts" } // Single orchestrator + ] + } + ] +} +``` + +**Note:** If first hook hangs, second won't run. Keep hooks fast! + +--- + +### Matcher Patterns + +`"matcher"` field filters which events trigger hook: + +```json +{ + "PostToolUse": [ + { + "matcher": "Bash", // Only Bash tool executions + "hooks": [...] + }, + { + "matcher": "*", // All tool executions + "hooks": [...] + } + ] +} +``` + +**Patterns:** +- `"*"` - All events +- `"Bash"` - Specific tool name +- `""` - Empty (all events, same as `*`) + +--- + +### Hook Data Payloads by Event Type + +**SessionStart:** +```typescript +{ + session_id: string; + transcript_path: string; + hook_event_name: "SessionStart"; + cwd: string; +} +``` + +**UserPromptSubmit:** +```typescript +{ + session_id: string; + transcript_path: string; + hook_event_name: "UserPromptSubmit"; + prompt: string; // The user's prompt text +} +``` + +**PreToolUse:** +```typescript +{ + session_id: string; + transcript_path: string; + hook_event_name: "PreToolUse"; + tool_name: string; + tool_input: any; // Tool parameters +} +``` + +**PostToolUse:** +```typescript +{ + session_id: string; + transcript_path: string; + hook_event_name: "PostToolUse"; + tool_name: string; + tool_input: any; + tool_output: any; // Tool result + error?: string; // If tool failed +} +``` + +**Stop:** +```typescript +{ + session_id: string; + transcript_path: string; + hook_event_name: "Stop"; +} +``` + +**SessionEnd:** +```typescript +{ + conversation_id: string; // Note: different field name + timestamp: string; +} +``` + +--- + +## Related Documentation + +- **Voice System:** `~/.opencode/VoiceServer/SKILL.md` +- **Agent System:** `~/.opencode/skills/Agents/SKILL.md` +- **History/Memory:** `~/.opencode/PAI/MEMORYSYSTEM.md` + +--- + +## Quick Reference Card + +``` +HOOK LIFECYCLE: +1. Event occurs (SessionStart, Stop, etc.) +2. Claude Code writes hook data to stdin +3. Hook script executes +4. Hook reads stdin (with timeout) +5. Hook performs actions (voice, capture, etc.) +6. Hook exits 0 (always succeeds) +7. Claude Code continues + +HOOKS BY EVENT (22 hooks total): + +SESSION START (2 hooks): + KittyEnvPersist.hook.ts Persist Kitty env vars + tab reset + LoadContext.hook.ts Dynamic context injection (relationship, learning, work) + +SESSION END (5 hooks): + WorkCompletionLearning.hook.ts Work/learning capture to MEMORY/ + SessionCleanup.hook.ts Mark WORK dir complete, clear state, reset tab + RelationshipMemory.hook.ts Relationship context to MEMORY/RELATIONSHIP/ + UpdateCounts.hook.ts Refresh system counts (skills, hooks, signals) + IntegrityCheck.hook.ts System integrity checks + +USER PROMPT SUBMIT (3 hooks): + RatingCapture.hook.ts Unified rating capture (explicit + implicit) + UpdateTabTitle.hook.ts Tab title + working state (orange) + SessionAutoName.hook.ts Auto-name session from first prompt + +STOP (5 hooks): + LastResponseCache.hook.ts Cache response for RatingCapture bridge + ResponseTabReset.hook.ts Tab title/color reset after response + VoiceCompletion.hook.ts Voice TTS (main sessions only) + DocIntegrity.hook.ts Cross-ref + semantic drift checks + AlgorithmTab.hook.ts Algorithm phase + progress in tab + +PRE TOOL USE (4 hooks): + SecurityValidator.hook.ts Security validation [Bash, Edit, Write, Read] + SetQuestionTab.hook.ts Tab state on question [AskUserQuestion] + AgentExecutionGuard.hook.ts Agent spawn guardrails [Task] + SkillGuard.hook.ts Skill invocation validation [Skill] + +POST TOOL USE (2 hooks): + QuestionAnswered.hook.ts Post-question tab reset [AskUserQuestion] + PRDSync.hook.ts PRD → work.json sync [Write, Edit] + +KEY FILES: +~/.opencode/settings.json Hook configuration +~/.opencode/hooks/ Hook scripts (22 files) +~/.opencode/hooks/handlers/ Handler modules (6 files) +~/.opencode/hooks/lib/ Shared libraries (13 files) +~/.opencode/hooks/lib/learning-utils.ts Learning categorization +~/.opencode/hooks/lib/time.ts PST timestamp utilities +~/.opencode/hooks/lib/event-types.ts Typed event definitions (22 interfaces) +~/.opencode/hooks/lib/event-emitter.ts appendEvent() → events.jsonl +~/.opencode/MEMORY/WORK/ Work tracking +~/.opencode/MEMORY/LEARNING/ Learning captures +~/.opencode/MEMORY/STATE/ Runtime state +~/.opencode/MEMORY/STATE/events.jsonl Unified event log (append-only) + +INFERENCE TOOL (for hooks needing AI): +Path: ~/.opencode/PAI/Tools/Inference.ts +Import: import { inference } from '../PAI/Tools/Inference' +Levels: fast (haiku/15s) | standard (sonnet/30s) | smart (opus/90s) + +TAB STATE SYSTEM: +Inference: 🧠… Orange #B35A00 (AI thinking) +Working: ⚙️… Orange #B35A00 (processing) +Completed: Green #022800 (task done) +Awaiting: ? Teal #0D4F4F (needs input) +Error: ! Orange #B35A00 (problem detected) +Active Tab: Always Dark Blue #002B80 (state colors = inactive only) + +VOICE SERVER: +URL: http://localhost:8888/notify +Payload: {"message":"...", "voice_id":"...", "title":"..."} +Configure voice IDs in individual agent files (`agents/*.md` persona frontmatter) + +``` + +--- + +## Shared Libraries + +The hook system uses shared TypeScript libraries to eliminate code duplication: + +### `hooks/lib/learning-utils.ts` +Shared learning categorization logic. + +```typescript +import { getLearningCategory, isLearningCapture } from './lib/learning-utils'; + +// Categorize learning as SYSTEM (tooling/infra) or ALGORITHM (task execution) +const category = getLearningCategory(content, comment); +// Returns: 'SYSTEM' | 'ALGORITHM' + +// Check if response contains learning indicators +const isLearning = isLearningCapture(text, summary, analysis); +// Returns: boolean (true if 2+ learning indicators found) +``` + +**Used by:** RatingCapture, WorkCompletionLearning + +### `hooks/lib/time.ts` +Shared PST timestamp utilities. + +```typescript +import { + getPSTTimestamp, // "2026-01-10 20:30:00 PST" + getPSTDate, // "2026-01-10" + getYearMonth, // "2026-01" + getISOTimestamp, // ISO8601 with offset + getFilenameTimestamp, // "2026-01-10-203000" + getPSTComponents // { year, month, day, hours, minutes, seconds } +} from './lib/time'; +``` + +**Used by:** RatingCapture, WorkCompletionLearning, SessionSummary + +### `hooks/lib/identity.ts` +Identity and principal configuration from settings.json. + +```typescript +import { getIdentity, getPrincipal, getDAName, getPrincipalName, getVoiceId } from './lib/identity'; + +const identity = getIdentity(); // { name, fullName, displayName, voiceId, color } +const principal = getPrincipal(); // { name, pronunciation, timezone } +``` + +**Used by:** handlers/VoiceNotification.ts, RatingCapture, handlers/TabState.ts + +### `PAI/Tools/Inference.ts` +Unified AI inference with three run levels. + +```typescript +import { inference } from '../PAI/Tools/Inference'; + +// Fast (Haiku) - quick tasks, 15s timeout +const result = await inference({ + systemPrompt: 'Summarize in 3 words', + userPrompt: text, + level: 'fast', +}); + +// Standard (Sonnet) - balanced reasoning, 30s timeout +const result = await inference({ + systemPrompt: 'Analyze sentiment', + userPrompt: text, + level: 'standard', + expectJson: true, +}); + +// Smart (Opus) - deep reasoning, 90s timeout +const result = await inference({ + systemPrompt: 'Strategic analysis', + userPrompt: text, + level: 'smart', +}); + +// Result shape +interface InferenceResult { + success: boolean; + output: string; + parsed?: unknown; // if expectJson: true + error?: string; + latencyMs: number; + level: 'fast' | 'standard' | 'smart'; +} +``` + +**Used by:** RatingCapture, UpdateTabTitle, SessionAutoName + +--- + +## Unified Event System + +Alongside existing filesystem state writes (algorithm-state JSON, PRDs, session-names.json, etc.), hooks can emit structured events to a single append-only JSONL log. This provides a unified observability layer without replacing any existing state management. + +### Components + +| File | Purpose | +|------|---------| +| `${PAI_DIR}/hooks/lib/event-types.ts` | TypeScript discriminated union of all PAI event types (22 interfaces covering algorithm, work, session, rating, learning, voice, PRD, doc, build, system, tab, hook error, and custom events) | +| `${PAI_DIR}/hooks/lib/event-emitter.ts` | `appendEvent()` utility that writes typed events to `${PAI_DIR}/MEMORY/STATE/events.jsonl` | + +### Usage in Hooks + +Hooks call `appendEvent()` as a secondary write **alongside** their existing state writes. The emitter is synchronous, fire-and-forget, and silently swallows errors so it never blocks or crashes a hook. + +```typescript +import { appendEvent } from './lib/event-emitter'; + +// Inside an existing hook, AFTER the normal state write: +appendEvent({ type: 'work.created', source: 'PRDSync', slug: 'my-task' }); +``` + +### Event Structure + +Every event has a common base shape plus type-specific fields: +- `timestamp` (ISO 8601) -- auto-injected by `appendEvent()` +- `session_id` -- auto-injected from `CLAUDE_SESSION_ID` env +- `source` -- the hook or handler name that emitted the event +- `type` -- dot-separated topic (e.g., `algorithm.phase`, `work.created`, `voice.sent`, `rating.captured`) + +Events use a dot-separated topic hierarchy for filtering. A `custom.*` escape hatch allows arbitrary extension without modifying the type system. + +### Event Type Categories + +| Category | Types | Emitting Hooks | +|----------|-------|----------------| +| `work.*` | created, completed | PRDSync, SessionCleanup | +| `session.*` | named, completed | SessionCleanup | +| `rating.*` | captured | RatingCapture | +| `learning.*` | captured | WorkCompletionLearning | +| `voice.*` | sent | VoiceNotification | +| `prd.*` | synced | PRDSync | +| `doc.*` | integrity | DocIntegrity | +| `build.*` | rebuild | BuildCLAUDE (SessionStart handler) | +| `system.*` | integrity | IntegrityCheck | +| `settings.*` | counts_updated | UpdateCounts | +| `tab.*` | updated | TabState, UpdateTabTitle | +| `hook.*` | error | Any hook (error reporting) | +| `custom.*` | user-defined | Extensibility escape hatch | + +### Consuming Events + +```bash +# Live tail (real-time monitoring) +tail -f ~/.opencode/MEMORY/STATE/events.jsonl | jq + +# Filter by type +tail -f ~/.opencode/MEMORY/STATE/events.jsonl | jq 'select(.type | startswith("algorithm."))' + +# Programmatic (Node/Bun fs.watch) +import { watch } from 'fs'; +import { getEventsPath } from './hooks/lib/event-emitter'; +watch(getEventsPath(), (eventType) => { /* read new lines */ }); +``` + +### Key Principles + +- **Additive only** -- events supplement existing state files, they never replace them +- **Append-only** -- `events.jsonl` is an immutable log, never rewritten or truncated by hooks +- **Graceful failure** -- write errors are swallowed; events are observability, not critical path +- **One file** -- all event types go to a single `events.jsonl` for simple tailing and watching + +--- + +**Last Updated:** 2026-02-25 +**Status:** Production - 15 hooks emitting 22 event types across 14 categories +**Maintainer:** PAI System diff --git a/.opencode/PAI/doc-dependencies.json b/.opencode/PAI/doc-dependencies.json new file mode 100644 index 00000000..b1c5a634 --- /dev/null +++ b/.opencode/PAI/doc-dependencies.json @@ -0,0 +1,178 @@ +{ + "$schema": "doc-dependencies-schema", + "version": "1.0.0", + "description": "Defines authoritative relationships between PAI documentation files for cross-reference integrity", + + "authoritative_docs": { + "PAISYSTEMARCHITECTURE.md": { + "description": "Master architecture document - source of truth for all system design", + "tier": 1, + "sections": { + "cloud-execution": { + "heading": "## Cloud Execution Architecture (Arbol)", + "line_range_hint": [400, 460], + "dependents": ["ACTIONS.md", "PIPELINES.md", "FLOWS.md"], + "key_facts": [ + "Worker naming: arbol-[apf]-{name}", + "Two-tier model: V8 isolate vs Sandbox", + "Auth: Bearer token via shared/auth.ts", + "Service bindings for internal calls" + ] + }, + "skill-system": { + "heading": "## Skill System Architecture", + "dependents": ["SKILLSYSTEM.md"] + }, + "hook-system": { + "heading": "## Hook System Architecture", + "dependents": ["THEHOOKSYSTEM.md"] + }, + "memory-system": { + "heading": "## Memory System Architecture", + "dependents": ["MEMORYSYSTEM.md"] + }, + "agent-system": { + "heading": "## Agent System Architecture", + "dependents": ["PAIAGENTSYSTEM.md"] + }, + "notification-system": { + "heading": "## Notification System Architecture", + "dependents": ["THENOTIFICATIONSYSTEM.md"] + }, + "security": { + "heading": "## Security Architecture", + "dependents": ["PAISECURITYSYSTEM/ARCHITECTURE.md"] + } + } + }, + + "ACTIONS.md": { + "description": "Action system documentation", + "tier": 2, + "sections": { + "deployed-workers": { + "heading": "## Deployed Workers", + "dependents": ["../ACTIONS/README.md"], + "key_facts": ["Worker URLs", "Worker types"] + }, + "naming-convention": { + "heading": "## Naming Convention", + "dependents": ["PIPELINES.md", "FLOWS.md"], + "key_facts": ["A_ prefix", "UPPER_SNAKE_CASE"] + } + }, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "PIPELINES.md": { + "description": "Pipeline system documentation", + "tier": 2, + "sections": { + "naming-convention": { + "heading": "## Naming Convention", + "dependents": ["FLOWS.md"], + "key_facts": ["P_ prefix"] + } + }, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "FLOWS.md": { + "description": "Flow system documentation", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "SKILLSYSTEM.md": { + "description": "Skill system detailed documentation", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "MEMORYSYSTEM.md": { + "description": "Memory system detailed documentation", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "THEHOOKSYSTEM.md": { + "description": "Hook system detailed documentation", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "PAIAGENTSYSTEM.md": { + "description": "Agent system detailed documentation", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "THEDELEGATIONSYSTEM.md": { + "description": "Delegation patterns documentation", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "THENOTIFICATIONSYSTEM.md": { + "description": "Notification system documentation", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + + "FEEDSYSTEM.md": { + "description": "Feed/intelligence system documentation", + "tier": 2 + }, + + "DOCUMENTATIONINDEX.md": { + "description": "Meta-index of all documentation - must be updated when docs added/removed", + "tier": 2, + "special": "meta-index", + "tracks_all": true + } + }, + + "readme_mappings": { + "ACTIONS.md": "../ACTIONS/README.md", + "PIPELINES.md": "../PIPELINES/README.md", + "FLOWS.md": "../FLOWS/README.md" + }, + + "changelog_config": { + "max_entries": 20, + "archive_location": "MEMORY/PAISYSTEMUPDATES/", + "format": "| {date} | {change} | {author} | {related} |" + }, + + "integrity_rules": [ + { + "id": "naming-consistency", + "description": "Action/Pipeline/Flow naming conventions must be consistent across all docs", + "check": "regex_match", + "sources": ["PAISYSTEMARCHITECTURE.md"], + "targets": ["ACTIONS.md", "PIPELINES.md", "FLOWS.md"], + "patterns": { + "action_prefix": "A_", + "pipeline_prefix": "P_", + "flow_prefix": "F_", + "worker_pattern": "arbol-[apf]-" + } + }, + { + "id": "auth-consistency", + "description": "Authentication method must be consistent", + "check": "key_fact", + "fact": "Bearer token authentication", + "sources": ["PAISYSTEMARCHITECTURE.md"], + "targets": ["ACTIONS.md", "FLOWS.md"] + }, + { + "id": "worker-list-sync", + "description": "Deployed worker lists should match between related docs", + "check": "section_hash", + "section": "## Deployed Workers", + "sources": ["ACTIONS.md"], + "targets": ["FLOWS.md"] + } + ] +} From 6405a7dc0e7f94cd6a389127147bd54f118b563a Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:40:06 -0400 Subject: [PATCH 3/8] refactor: rename voice-server/ to VoiceServer/ + add missing files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed: voice-server/ → VoiceServer/ (CamelCase, matches PAI 4.0.3) Added missing files from PAI 4.0.3: - install.sh, start.sh, stop.sh, restart.sh, status.sh, uninstall.sh - voices.json, pronunciations.json - menubar/ directory All .claude/ references replaced with .opencode/ in copied files. --- .../{voice-server => VoiceServer}/README.md | 0 .opencode/VoiceServer/install.sh | 206 ++++++++++++++++++ .../logs/README.md | 0 .../VoiceServer/menubar/install-menubar.sh | 115 ++++++++++ .opencode/VoiceServer/menubar/pai-voice.5s.sh | 49 +++++ .opencode/VoiceServer/pronunciations.json | 7 + .opencode/VoiceServer/restart.sh | 23 ++ .../{voice-server => VoiceServer}/server.ts | 0 .opencode/VoiceServer/start.sh | 51 +++++ .opencode/VoiceServer/status.sh | 97 +++++++++ .opencode/VoiceServer/stop.sh | 36 +++ .opencode/VoiceServer/uninstall.sh | 83 +++++++ .opencode/VoiceServer/voices.json | 107 +++++++++ 13 files changed, 774 insertions(+) rename .opencode/{voice-server => VoiceServer}/README.md (100%) create mode 100755 .opencode/VoiceServer/install.sh rename .opencode/{voice-server => VoiceServer}/logs/README.md (100%) create mode 100755 .opencode/VoiceServer/menubar/install-menubar.sh create mode 100755 .opencode/VoiceServer/menubar/pai-voice.5s.sh create mode 100644 .opencode/VoiceServer/pronunciations.json create mode 100755 .opencode/VoiceServer/restart.sh rename .opencode/{voice-server => VoiceServer}/server.ts (100%) create mode 100755 .opencode/VoiceServer/start.sh create mode 100755 .opencode/VoiceServer/status.sh create mode 100755 .opencode/VoiceServer/stop.sh create mode 100755 .opencode/VoiceServer/uninstall.sh create mode 100755 .opencode/VoiceServer/voices.json diff --git a/.opencode/voice-server/README.md b/.opencode/VoiceServer/README.md similarity index 100% rename from .opencode/voice-server/README.md rename to .opencode/VoiceServer/README.md diff --git a/.opencode/VoiceServer/install.sh b/.opencode/VoiceServer/install.sh new file mode 100755 index 00000000..15b6c83e --- /dev/null +++ b/.opencode/VoiceServer/install.sh @@ -0,0 +1,206 @@ +#!/bin/bash + +# Voice Server Installation Script +# This script installs the voice server as a macOS service + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SERVICE_NAME="com.pai.voice-server" +PLIST_PATH="$HOME/Library/LaunchAgents/${SERVICE_NAME}.plist" +LOG_PATH="$HOME/Library/Logs/pai-voice-server.log" +ENV_FILE="$HOME/.env" + +echo -e "${BLUE}=====================================================${NC}" +echo -e "${BLUE} PAI Voice Server Installation${NC}" +echo -e "${BLUE}=====================================================${NC}" +echo + +# Check for Bun +echo -e "${YELLOW}> Checking prerequisites...${NC}" +if ! command -v bun &> /dev/null; then + echo -e "${RED}X Bun is not installed${NC}" + echo " Please install Bun first:" + echo " curl -fsSL https://bun.sh/install | bash" + exit 1 +fi +echo -e "${GREEN}OK Bun is installed${NC}" + +# Check for existing installation +if launchctl list | grep -q "$SERVICE_NAME" 2>/dev/null; then + echo -e "${YELLOW}! Voice server is already installed${NC}" + read -p "Do you want to reinstall? (y/n): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}> Stopping existing service...${NC}" + launchctl unload "$PLIST_PATH" 2>/dev/null || true + echo -e "${GREEN}OK Existing service stopped${NC}" + else + echo "Installation cancelled" + exit 0 + fi +fi + +# Check for ElevenLabs configuration +echo -e "${YELLOW}> Checking ElevenLabs configuration...${NC}" +if [ -f "$ENV_FILE" ] && grep -q "ELEVENLABS_API_KEY=" "$ENV_FILE"; then + API_KEY=$(grep "ELEVENLABS_API_KEY=" "$ENV_FILE" | cut -d'=' -f2) + if [ "$API_KEY" != "your_api_key_here" ] && [ -n "$API_KEY" ]; then + echo -e "${GREEN}OK ElevenLabs API key configured${NC}" + ELEVENLABS_CONFIGURED=true + else + echo -e "${YELLOW}! ElevenLabs API key not configured${NC}" + echo " Voice server will use macOS 'say' command as fallback" + ELEVENLABS_CONFIGURED=false + fi +else + echo -e "${YELLOW}! No ElevenLabs configuration found${NC}" + echo " Voice server will use macOS 'say' command as fallback" + ELEVENLABS_CONFIGURED=false +fi + +if [ "$ELEVENLABS_CONFIGURED" = false ]; then + echo + echo "To enable AI voices, add your ElevenLabs API key to ~/.env:" + echo " echo 'ELEVENLABS_API_KEY=your_api_key_here' >> ~/.env" + echo " Get a free key at: https://elevenlabs.io" + echo +fi + +# Create LaunchAgent plist +echo -e "${YELLOW}> Creating LaunchAgent configuration...${NC}" +mkdir -p "$HOME/Library/LaunchAgents" + +cat > "$PLIST_PATH" << EOF + + + + + Label + ${SERVICE_NAME} + + ProgramArguments + + $(which bun) + run + ${SCRIPT_DIR}/server.ts + + + WorkingDirectory + ${SCRIPT_DIR} + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + + + StandardOutPath + ${LOG_PATH} + + StandardErrorPath + ${LOG_PATH} + + EnvironmentVariables + + HOME + ${HOME} + PATH + /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${HOME}/.bun/bin + + + +EOF + +echo -e "${GREEN}OK LaunchAgent configuration created${NC}" + +# Load the LaunchAgent +echo -e "${YELLOW}> Starting voice server service...${NC}" +launchctl load "$PLIST_PATH" 2>/dev/null || { + echo -e "${RED}X Failed to load LaunchAgent${NC}" + echo " Try manually: launchctl load $PLIST_PATH" + exit 1 +} + +# Wait for server to start +sleep 2 + +# Test the server +echo -e "${YELLOW}> Testing voice server...${NC}" +if curl -s -f -X GET http://localhost:8888/health > /dev/null 2>&1; then + echo -e "${GREEN}OK Voice server is running${NC}" + + # Send test notification + echo -e "${YELLOW}> Sending test notification...${NC}" + curl -s -X POST http://localhost:8888/notify \ + -H "Content-Type: application/json" \ + -d '{"message": "Voice server installed successfully"}' > /dev/null + echo -e "${GREEN}OK Test notification sent${NC}" +else + echo -e "${RED}X Voice server is not responding${NC}" + echo " Check logs at: $LOG_PATH" + echo " Try running manually: bun run $SCRIPT_DIR/server.ts" + exit 1 +fi + +# Show summary +echo +echo -e "${GREEN}=====================================================${NC}" +echo -e "${GREEN} Installation Complete!${NC}" +echo -e "${GREEN}=====================================================${NC}" +echo +echo -e "${BLUE}Service Information:${NC}" +echo " - Service: $SERVICE_NAME" +echo " - Status: Running" +echo " - Port: 8888" +echo " - Logs: $LOG_PATH" + +if [ "$ELEVENLABS_CONFIGURED" = true ]; then + echo " - Voice: ElevenLabs AI" +else + echo " - Voice: macOS Say (fallback)" +fi + +echo +echo -e "${BLUE}Management Commands:${NC}" +echo " - Status: ./status.sh" +echo " - Stop: ./stop.sh" +echo " - Start: ./start.sh" +echo " - Restart: ./restart.sh" +echo " - Uninstall: ./uninstall.sh" + +echo +echo -e "${BLUE}Test the server:${NC}" +echo " curl -X POST http://localhost:8888/notify \\" +echo " -H 'Content-Type: application/json' \\" +echo " -d '{\"message\": \"Hello from PAI\"}'" + +echo +echo -e "${GREEN}The voice server will now start automatically when you log in.${NC}" + +# Ask about menu bar indicator +echo +read -p "Would you like to install a menu bar indicator? (y/n): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}> Installing menu bar indicator...${NC}" + if [ -f "$SCRIPT_DIR/menubar/install-menubar.sh" ]; then + chmod +x "$SCRIPT_DIR/menubar/install-menubar.sh" + "$SCRIPT_DIR/menubar/install-menubar.sh" + else + echo -e "${YELLOW}! Menu bar installer not found${NC}" + echo " You can install it manually later from:" + echo " $SCRIPT_DIR/menubar/install-menubar.sh" + fi +fi diff --git a/.opencode/voice-server/logs/README.md b/.opencode/VoiceServer/logs/README.md similarity index 100% rename from .opencode/voice-server/logs/README.md rename to .opencode/VoiceServer/logs/README.md diff --git a/.opencode/VoiceServer/menubar/install-menubar.sh b/.opencode/VoiceServer/menubar/install-menubar.sh new file mode 100755 index 00000000..c0107ed7 --- /dev/null +++ b/.opencode/VoiceServer/menubar/install-menubar.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Install Menu Bar Indicator for Voice Server + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +MENUBAR_SCRIPT="$SCRIPT_DIR/pai-voice.5s.sh" + +echo -e "${BLUE}=====================================================${NC}" +echo -e "${BLUE} PAI Voice Menu Bar Installation${NC}" +echo -e "${BLUE}=====================================================${NC}" +echo + +# Check if SwiftBar is installed +if [ -d "/Applications/SwiftBar.app" ]; then + echo -e "${GREEN}OK SwiftBar is installed${NC}" + MENUBAR_APP="SwiftBar" + PLUGIN_DIR="$HOME/Library/Application Support/SwiftBar/Plugins" +elif [ -d "/Applications/BitBar.app" ]; then + echo -e "${GREEN}OK BitBar is installed${NC}" + MENUBAR_APP="BitBar" + # Check for BitBar plugin directory + if [ -d "$HOME/Documents/BitBarPlugins" ]; then + PLUGIN_DIR="$HOME/Documents/BitBarPlugins" + elif [ -d "$HOME/BitBar" ]; then + PLUGIN_DIR="$HOME/BitBar" + else + PLUGIN_DIR="$HOME/Documents/BitBarPlugins" + echo -e "${YELLOW}> Creating BitBar plugin directory...${NC}" + mkdir -p "$PLUGIN_DIR" + fi +else + echo -e "${RED}X Neither SwiftBar nor BitBar is installed${NC}" + echo + echo "Please install SwiftBar (recommended) or BitBar first:" + echo + echo "Option 1: Install SwiftBar (Recommended)" + echo " brew install --cask swiftbar" + echo " Or download from: https://github.com/swiftbar/SwiftBar/releases" + echo + echo "Option 2: Install BitBar" + echo " brew install --cask bitbar" + echo " Or download from: https://getbitbar.com" + echo + exit 1 +fi + +# Make script executable +chmod +x "$MENUBAR_SCRIPT" + +# Create plugin directory if it doesn't exist +mkdir -p "$PLUGIN_DIR" + +# Copy or link the script +echo -e "${YELLOW}> Installing menu bar plugin...${NC}" + +# Remove existing plugin if it exists +rm -f "$PLUGIN_DIR/pai-voice.5s.sh" 2>/dev/null || true + +# Create symbolic link to our script +ln -s "$MENUBAR_SCRIPT" "$PLUGIN_DIR/pai-voice.5s.sh" + +echo -e "${GREEN}OK Menu bar plugin installed${NC}" + +# Refresh SwiftBar/BitBar +if [ "$MENUBAR_APP" = "SwiftBar" ]; then + echo -e "${YELLOW}> Refreshing SwiftBar...${NC}" + if pgrep -x "SwiftBar" > /dev/null; then + # SwiftBar refresh via URL scheme + open -g "swiftbar://refreshall" + echo -e "${GREEN}OK SwiftBar refreshed${NC}" + else + echo -e "${YELLOW}> Starting SwiftBar...${NC}" + open -a SwiftBar + sleep 2 + echo -e "${GREEN}OK SwiftBar started${NC}" + fi +else + echo -e "${YELLOW}> Refreshing BitBar...${NC}" + if pgrep -x "BitBar" > /dev/null; then + killall BitBar 2>/dev/null || true + sleep 1 + fi + open -a BitBar + echo -e "${GREEN}OK BitBar started${NC}" +fi + +echo +echo -e "${GREEN}=====================================================${NC}" +echo -e "${GREEN} Menu Bar Installation Complete${NC}" +echo -e "${GREEN}=====================================================${NC}" +echo +echo -e "${BLUE}You should now see a microphone icon in your menu bar!${NC}" +echo +echo "The icon shows:" +echo " - Colored microphone - Server is running" +echo " - Gray microphone - Server is stopped" +echo +echo "Click the icon to:" +echo " - Start/Stop the server" +echo " - View status and logs" +echo " - Test voice output" +echo +echo -e "${YELLOW}Note:${NC} If you don't see the icon, you may need to:" +echo " 1. Open $MENUBAR_APP preferences" +echo " 2. Set the plugin folder to: $PLUGIN_DIR" +echo " 3. Restart $MENUBAR_APP" diff --git a/.opencode/VoiceServer/menubar/pai-voice.5s.sh b/.opencode/VoiceServer/menubar/pai-voice.5s.sh new file mode 100755 index 00000000..d799df58 --- /dev/null +++ b/.opencode/VoiceServer/menubar/pai-voice.5s.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Voice Server Menu Bar Indicator +# For BitBar/SwiftBar - updates every 5 seconds + +# Get the VoiceServer directory +PAI_DIR="${PAI_DIR:-$HOME/.claude}" +VOICE_SERVER_DIR="$PAI_DIR/VoiceServer" + +# Check if server is running +if curl -s -f http://localhost:8888/health > /dev/null 2>&1; then + # Server is running - show green indicator with size + echo "🎙️ | size=18" + echo "---" + echo "Voice Server: ✅ Running" + + # Check for ElevenLabs + if [ -f ~/.env ] && grep -q "ELEVENLABS_API_KEY=" ~/.env 2>/dev/null; then + API_KEY=$(grep "ELEVENLABS_API_KEY=" ~/.env | cut -d'=' -f2) + if [ "$API_KEY" != "your_api_key_here" ] && [ -n "$API_KEY" ]; then + echo "Voice: ElevenLabs AI" + else + echo "Voice: macOS Say" + fi + else + echo "Voice: macOS Say" + fi + + echo "Port: 8888" + echo "---" + echo "Stop Server | bash='$VOICE_SERVER_DIR/stop.sh' terminal=false refresh=true" + echo "Restart Server | bash='$VOICE_SERVER_DIR/restart.sh' terminal=false refresh=true" +else + # Server is not running - show gray indicator with size + echo "🎙️⚫ | size=18" + echo "---" + echo "Voice Server: ⚫ Stopped" + echo "---" + echo "Start Server | bash='$VOICE_SERVER_DIR/start.sh' terminal=false refresh=true" +fi + +echo "---" +echo "Check Status | bash='$VOICE_SERVER_DIR/status.sh' terminal=true" +echo "View Logs | bash='tail -f ~/Library/Logs/pai-voice-server.log' terminal=true" +echo "---" +echo "Test Voice | bash='curl -X POST http://localhost:8888/notify -H \"Content-Type: application/json\" -d \"{\\\"message\\\":\\\"Testing voice server\\\"}\"' terminal=false" +echo "---" +echo "Open Voice Server Folder | bash='open $VOICE_SERVER_DIR'" +echo "Uninstall | bash='$VOICE_SERVER_DIR/uninstall.sh' terminal=true" diff --git a/.opencode/VoiceServer/pronunciations.json b/.opencode/VoiceServer/pronunciations.json new file mode 100644 index 00000000..5d32e711 --- /dev/null +++ b/.opencode/VoiceServer/pronunciations.json @@ -0,0 +1,7 @@ +{ + "_comment": "TTS pronunciation overrides. Each entry maps a display term to its phonetic TTS spelling. Uses word-boundary matching to avoid mid-word replacements. Source of truth: skills/PAI/USER/PRONUNCIATIONS.md", + "replacements": [ + { "term": "PAI", "phonetic": "pie", "note": "Personal AI Infrastructure - rhymes with sky" }, + { "term": "ISC", "phonetic": "I S C", "note": "Ideal State Criteria - spell out" } + ] +} diff --git a/.opencode/VoiceServer/restart.sh b/.opencode/VoiceServer/restart.sh new file mode 100755 index 00000000..02d84255 --- /dev/null +++ b/.opencode/VoiceServer/restart.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Restart the Voice Server + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Colors +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +NC='\033[0m' + +echo -e "${YELLOW}> Restarting Voice Server...${NC}" + +# Stop the server +"$SCRIPT_DIR/stop.sh" + +# Wait a moment +sleep 2 + +# Start the server +"$SCRIPT_DIR/start.sh" + +echo -e "${GREEN}OK Voice server restarted${NC}" diff --git a/.opencode/voice-server/server.ts b/.opencode/VoiceServer/server.ts similarity index 100% rename from .opencode/voice-server/server.ts rename to .opencode/VoiceServer/server.ts diff --git a/.opencode/VoiceServer/start.sh b/.opencode/VoiceServer/start.sh new file mode 100755 index 00000000..3e1459cc --- /dev/null +++ b/.opencode/VoiceServer/start.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Start the Voice Server + +SERVICE_NAME="com.pai.voice-server" +PLIST_PATH="$HOME/Library/LaunchAgents/${SERVICE_NAME}.plist" +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${YELLOW}> Starting Voice Server...${NC}" + +# Check if LaunchAgent exists +if [ ! -f "$PLIST_PATH" ]; then + echo -e "${RED}X Service not installed${NC}" + echo " Run ./install.sh first to install the service" + exit 1 +fi + +# Check if already running +if launchctl list | grep -q "$SERVICE_NAME" 2>/dev/null; then + echo -e "${YELLOW}! Voice server is already running${NC}" + echo " To restart, use: ./restart.sh" + exit 0 +fi + +# Load the service +launchctl load "$PLIST_PATH" 2>/dev/null + +if [ $? -eq 0 ]; then + # Wait for server to start + sleep 2 + + # Test if server is responding + if curl -s -f -X GET http://localhost:8888/health > /dev/null 2>&1; then + echo -e "${GREEN}OK Voice server started successfully${NC}" + echo " Port: 8888" + echo " Test: curl -X POST http://localhost:8888/notify -H 'Content-Type: application/json' -d '{\"message\":\"Test\"}'" + else + echo -e "${YELLOW}! Server started but not responding yet${NC}" + echo " Check logs: tail -f ~/Library/Logs/pai-voice-server.log" + fi +else + echo -e "${RED}X Failed to start voice server${NC}" + echo " Try running manually: bun run $SCRIPT_DIR/server.ts" + exit 1 +fi diff --git a/.opencode/VoiceServer/status.sh b/.opencode/VoiceServer/status.sh new file mode 100755 index 00000000..3e98d3d0 --- /dev/null +++ b/.opencode/VoiceServer/status.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +# Check status of Voice Server + +SERVICE_NAME="com.pai.voice-server" +PLIST_PATH="$HOME/Library/LaunchAgents/${SERVICE_NAME}.plist" +LOG_PATH="$HOME/Library/Logs/pai-voice-server.log" +ENV_FILE="$HOME/.env" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}=====================================================${NC}" +echo -e "${BLUE} PAI Voice Server Status${NC}" +echo -e "${BLUE}=====================================================${NC}" +echo + +# Check LaunchAgent +echo -e "${BLUE}Service Status:${NC}" +if launchctl list | grep -q "$SERVICE_NAME" 2>/dev/null; then + PID=$(launchctl list | grep "$SERVICE_NAME" | awk '{print $1}') + if [ "$PID" != "-" ]; then + echo -e " ${GREEN}OK Service is loaded (PID: $PID)${NC}" + else + echo -e " ${YELLOW}! Service is loaded but not running${NC}" + fi +else + echo -e " ${RED}X Service is not loaded${NC}" +fi + +# Check if server is responding +echo +echo -e "${BLUE}Server Status:${NC}" +if curl -s -f -X GET http://localhost:8888/health > /dev/null 2>&1; then + echo -e " ${GREEN}OK Server is responding on port 8888${NC}" + + # Get health info + HEALTH=$(curl -s http://localhost:8888/health) + echo " Response: $HEALTH" +else + echo -e " ${RED}X Server is not responding${NC}" +fi + +# Check port binding +echo +echo -e "${BLUE}Port Status:${NC}" +if lsof -i :8888 > /dev/null 2>&1; then + PROCESS=$(lsof -i :8888 | grep LISTEN | head -1) + echo -e " ${GREEN}OK Port 8888 is in use${NC}" + echo "$PROCESS" | awk '{print " Process: " $1 " (PID: " $2 ")"}' +else + echo -e " ${YELLOW}! Port 8888 is not in use${NC}" +fi + +# Check ElevenLabs configuration +echo +echo -e "${BLUE}Voice Configuration:${NC}" +if [ -f "$ENV_FILE" ] && grep -q "ELEVENLABS_API_KEY=" "$ENV_FILE"; then + API_KEY=$(grep "ELEVENLABS_API_KEY=" "$ENV_FILE" | cut -d'=' -f2) + if [ "$API_KEY" != "your_api_key_here" ] && [ -n "$API_KEY" ]; then + echo -e " ${GREEN}OK ElevenLabs API configured${NC}" + if grep -q "ELEVENLABS_VOICE_ID=" "$ENV_FILE"; then + VOICE_ID=$(grep "ELEVENLABS_VOICE_ID=" "$ENV_FILE" | cut -d'=' -f2) + echo " Voice ID: $VOICE_ID" + fi + else + echo -e " ${YELLOW}! Using macOS 'say' (no API key)${NC}" + fi +else + echo -e " ${YELLOW}! Using macOS 'say' (no configuration)${NC}" +fi + +# Check logs +echo +echo -e "${BLUE}Recent Logs:${NC}" +if [ -f "$LOG_PATH" ]; then + echo " Log file: $LOG_PATH" + echo " Last 5 lines:" + tail -5 "$LOG_PATH" | while IFS= read -r line; do + echo " $line" + done +else + echo -e " ${YELLOW}! No log file found${NC}" +fi + +# Show commands +echo +echo -e "${BLUE}Available Commands:${NC}" +echo " - Start: ./start.sh" +echo " - Stop: ./stop.sh" +echo " - Restart: ./restart.sh" +echo " - Logs: tail -f $LOG_PATH" +echo " - Test: curl -X POST http://localhost:8888/notify -H 'Content-Type: application/json' -d '{\"message\":\"Test\"}'" diff --git a/.opencode/VoiceServer/stop.sh b/.opencode/VoiceServer/stop.sh new file mode 100755 index 00000000..d5048582 --- /dev/null +++ b/.opencode/VoiceServer/stop.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Stop the Voice Server + +SERVICE_NAME="com.pai.voice-server" +PLIST_PATH="$HOME/Library/LaunchAgents/${SERVICE_NAME}.plist" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${YELLOW}> Stopping Voice Server...${NC}" + +# Check if service is loaded +if launchctl list | grep -q "$SERVICE_NAME" 2>/dev/null; then + # Unload the service + launchctl unload "$PLIST_PATH" 2>/dev/null + + if [ $? -eq 0 ]; then + echo -e "${GREEN}OK Voice server stopped successfully${NC}" + else + echo -e "${RED}X Failed to stop voice server${NC}" + exit 1 + fi +else + echo -e "${YELLOW}! Voice server is not running${NC}" +fi + +# Kill any remaining processes on port 8888 +if lsof -i :8888 > /dev/null 2>&1; then + echo -e "${YELLOW}> Cleaning up port 8888...${NC}" + lsof -ti :8888 | xargs kill -9 2>/dev/null + echo -e "${GREEN}OK Port 8888 cleared${NC}" +fi diff --git a/.opencode/VoiceServer/uninstall.sh b/.opencode/VoiceServer/uninstall.sh new file mode 100755 index 00000000..1f4cc8dc --- /dev/null +++ b/.opencode/VoiceServer/uninstall.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Uninstall Voice Server + +SERVICE_NAME="com.pai.voice-server" +PLIST_PATH="$HOME/Library/LaunchAgents/${SERVICE_NAME}.plist" +LOG_PATH="$HOME/Library/Logs/pai-voice-server.log" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}=====================================================${NC}" +echo -e "${BLUE} PAI Voice Server Uninstall${NC}" +echo -e "${BLUE}=====================================================${NC}" +echo + +# Confirm uninstall +echo -e "${YELLOW}This will:${NC}" +echo " - Stop the voice server" +echo " - Remove the LaunchAgent" +echo " - Keep your server files and configuration" +echo +read -p "Are you sure you want to uninstall? (y/n): " -n 1 -r +echo +echo + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Uninstall cancelled" + exit 0 +fi + +# Stop the service if running +echo -e "${YELLOW}> Stopping voice server...${NC}" +if launchctl list | grep -q "$SERVICE_NAME" 2>/dev/null; then + launchctl unload "$PLIST_PATH" 2>/dev/null + echo -e "${GREEN}OK Voice server stopped${NC}" +else + echo -e "${YELLOW} Service was not running${NC}" +fi + +# Remove LaunchAgent plist +echo -e "${YELLOW}> Removing LaunchAgent...${NC}" +if [ -f "$PLIST_PATH" ]; then + rm "$PLIST_PATH" + echo -e "${GREEN}OK LaunchAgent removed${NC}" +else + echo -e "${YELLOW} LaunchAgent file not found${NC}" +fi + +# Kill any remaining processes +if lsof -i :8888 > /dev/null 2>&1; then + echo -e "${YELLOW}> Cleaning up port 8888...${NC}" + lsof -ti :8888 | xargs kill -9 2>/dev/null + echo -e "${GREEN}OK Port 8888 cleared${NC}" +fi + +# Ask about logs +echo +read -p "Do you want to remove log files? (y/n): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + if [ -f "$LOG_PATH" ]; then + rm "$LOG_PATH" + echo -e "${GREEN}OK Log file removed${NC}" + fi +fi + +echo +echo -e "${GREEN}=====================================================${NC}" +echo -e "${GREEN} Uninstall Complete${NC}" +echo -e "${GREEN}=====================================================${NC}" +echo +echo -e "${BLUE}Notes:${NC}" +echo " - Your server files are still in: $(dirname "${BASH_SOURCE[0]}")" +echo " - Your ~/.env configuration is preserved" +echo " - To reinstall, run: ./install.sh" +echo +echo "To completely remove all files:" +echo " rm -rf $(dirname "${BASH_SOURCE[0]}")" diff --git a/.opencode/VoiceServer/voices.json b/.opencode/VoiceServer/voices.json new file mode 100755 index 00000000..6686e410 --- /dev/null +++ b/.opencode/VoiceServer/voices.json @@ -0,0 +1,107 @@ +{ + "$schema": "./voices-schema.json", + "default_rate": 175, + "default_volume": 0.8, + "voices": { + "default": { + "voice_id": "bIHbv24MWmeRgasZH58o", + "voice_name": "Default (Male)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.50, + "similarity_boost": 0.75, + "description": "Default male voice - balanced and professional", + "type": "Premium" + }, + "assistant": { + "voice_id": "bIHbv24MWmeRgasZH58o", + "voice_name": "Assistant (Male)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.50, + "similarity_boost": 0.75, + "description": "Primary assistant voice - warm and helpful", + "type": "Premium" + }, + "researcher": { + "voice_id": "MClEFoImJXBTgLwdLI5n", + "voice_name": "Researcher (Female)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.65, + "similarity_boost": 0.9, + "description": "Research agent voice - analytical and authoritative", + "type": "Premium" + }, + "engineer": { + "voice_id": "bIHbv24MWmeRgasZH58o", + "voice_name": "Engineer (Male)", + "rate_multiplier": 1.3, + "rate_wpm": 228, + "stability": 0.7, + "similarity_boost": 0.85, + "description": "Engineering voice - measured and precise", + "type": "Premium" + }, + "architect": { + "voice_id": "MClEFoImJXBTgLwdLI5n", + "voice_name": "Architect (Female)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.7, + "similarity_boost": 0.85, + "description": "Architecture voice - strategic and sophisticated", + "type": "Premium" + }, + "designer": { + "voice_id": "MClEFoImJXBTgLwdLI5n", + "voice_name": "Designer (Female)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.62, + "similarity_boost": 0.85, + "description": "Design voice - creative and discerning", + "type": "Premium" + }, + "analyst": { + "voice_id": "M563YhMmA0S8vEYwkgYa", + "voice_name": "Analyst (Neutral)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.65, + "similarity_boost": 0.85, + "description": "Analyst voice - thorough and balanced", + "type": "Premium" + }, + "writer": { + "voice_id": "MClEFoImJXBTgLwdLI5n", + "voice_name": "Writer (Female)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.6, + "similarity_boost": 0.8, + "description": "Writer voice - articulate and warm", + "type": "Premium" + }, + "security": { + "voice_id": "bIHbv24MWmeRgasZH58o", + "voice_name": "Security (Male)", + "rate_multiplier": 1.35, + "rate_wpm": 236, + "stability": 0.32, + "similarity_boost": 0.88, + "description": "Security voice - alert and focused", + "type": "Enhanced" + }, + "intern": { + "voice_id": "M563YhMmA0S8vEYwkgYa", + "voice_name": "Intern (Neutral)", + "rate_multiplier": 1.4, + "rate_wpm": 245, + "stability": 0.42, + "similarity_boost": 0.72, + "description": "Intern voice - enthusiastic and eager", + "type": "Premium" + } + } +} From dc27f9a874280bccc1630a31755dfb8a5fb1d9de Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:02:17 -0400 Subject: [PATCH 4/8] fix: restore missing files + remove legacy content from skills/PAI/ deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restored from git history (accidentally deleted with skills/PAI/): - PAI/THEPLUGINSYSTEM.md (OpenCode replacement for THEHOOKSYSTEM.md) - PAI/Tools/GenerateSkillIndex.ts + ValidateSkillStructure.ts + SkillSearch.ts - PAI/PIPELINES.md, PAI/doc-dependencies.json - PAISECURITYSYSTEM/HOOKS.md Removed legacy files NOT in 4.0.3 upstream: - PAI/BACKUPS.md, BROWSERAUTOMATION.md, CONSTITUTION.md, RESPONSEFORMAT.md, SCRAPINGREFERENCE.md, TERMINALTABS.md (all pre-v3.0 artifacts) - PAI/UPDATES/ directory (not in 4.0.3) - PAI/Workflows/ (11 legacy workflows not in 4.0.3 root) - PAI/USER/ reset to 4.0.3 template (README.md placeholders only, removed 47 personal template files that don't belong in public repo) Fixed references: THEHOOKSYSTEM → THEPLUGINSYSTEM in SKILL.md and DOCUMENTATIONINDEX.md Regenerated skill-index.json (52 skills, 7 categories) PAI/ root now matches 4.0.3 exactly, with only 2 justified deviations: - THEPLUGINSYSTEM.md (replaces THEHOOKSYSTEM.md per ADR-001) - MINIMAL_BOOTSTRAP.md (OpenCode lazy loading, created in v3.0 WP-C) --- .opencode/PAI/DOCUMENTATIONINDEX.md | 2 +- .opencode/PAI/SKILL.md | 2 +- .opencode/PAI/THEPLUGINSYSTEM.md | 498 ++++++++++++++++++ .opencode/PAI/Tools/GenerateSkillIndex.ts | 374 +++++++++++++ .opencode/PAI/Tools/SkillSearch.ts | 203 +++++++ .opencode/PAI/Tools/ValidateSkillStructure.ts | 353 +++++++++++++ .opencode/PAISECURITYSYSTEM/HOOKS.md | 254 +++++++++ .opencode/skills/skill-index.json | 80 +-- 8 files changed, 1713 insertions(+), 53 deletions(-) create mode 100644 .opencode/PAI/THEPLUGINSYSTEM.md create mode 100644 .opencode/PAI/Tools/GenerateSkillIndex.ts create mode 100644 .opencode/PAI/Tools/SkillSearch.ts create mode 100644 .opencode/PAI/Tools/ValidateSkillStructure.ts create mode 100644 .opencode/PAISECURITYSYSTEM/HOOKS.md diff --git a/.opencode/PAI/DOCUMENTATIONINDEX.md b/.opencode/PAI/DOCUMENTATIONINDEX.md index 59a2d5ea..db3dcf4c 100755 --- a/.opencode/PAI/DOCUMENTATIONINDEX.md +++ b/.opencode/PAI/DOCUMENTATIONINDEX.md @@ -67,7 +67,7 @@ See `SKILLSYSTEM.md` for complete documentation. - Voice notifications → VoiceServer (system alerts, agent feedback) **Configuration & Systems:** -- `THEHOOKSYSTEM.md` - Hook configuration | Triggers: "hooks configuration", "create custom hooks" +- `THEPLUGINSYSTEM.md` - Plugin system configuration | Triggers: "plugin system", "event handlers", "create custom plugins" - `MEMORYSYSTEM.md` - Memory documentation | Triggers: "memory system", "capture system", "work tracking", "session history" - `TERMINALTABS.md` - Terminal tab state system (colors + suffixes for working/completed/awaiting/error states) | Triggers: "tab colors", "tab state", "kitty tabs" diff --git a/.opencode/PAI/SKILL.md b/.opencode/PAI/SKILL.md index 7fca78fc..b30edea4 100644 --- a/.opencode/PAI/SKILL.md +++ b/.opencode/PAI/SKILL.md @@ -460,7 +460,7 @@ Critical PAI documentation organized by domain. Load on-demand based on context. | **System Architecture** | `SYSTEM/PAISYSTEMARCHITECTURE.md` | Core PAI design and principles | | **Memory System** | `SYSTEM/MEMORYSYSTEM.md` | WORK, STATE, LEARNING directories | | **Skill System** | `SYSTEM/SKILLSYSTEM.md` | How skills work, structure, triggers | -| **Hook System** | `SYSTEM/THEHOOKSYSTEM.md` | Event hooks, patterns, implementation | +| **Plugin System** | `SYSTEM/THEPLUGINSYSTEM.md` | Event-driven plugins, hooks, automation | | **Agent System** | `SYSTEM/PAIAGENTSYSTEM.md` | Agent types, spawning, delegation | | **Delegation** | `SYSTEM/THEDELEGATIONSYSTEM.md` | Background work, parallelization | | **Browser Automation** | `SYSTEM/BROWSERAUTOMATION.md` | Playwright, screenshots, testing | diff --git a/.opencode/PAI/THEPLUGINSYSTEM.md b/.opencode/PAI/THEPLUGINSYSTEM.md new file mode 100644 index 00000000..ddaa3e22 --- /dev/null +++ b/.opencode/PAI/THEPLUGINSYSTEM.md @@ -0,0 +1,498 @@ +# Plugin System + +**Event-Driven Automation Infrastructure for OpenCode** + +**Location:** `~/.opencode/plugins/` +**Configuration:** `~/.opencode/opencode.json` +**Status:** Active — All plugins running in production +**Version:** v3.0 (27 handlers, 9 libraries) + +--- + +## Overview + +The PAI plugin system is an event-driven automation infrastructure built on OpenCode's native plugin API. A single unified plugin (`pai-unified.ts`) routes all events to specialized handler modules across two layers: + +**Layer 1 — Hooks (active, can block/modify):** +- Context injection (bootstrap loading) +- Security validation (block dangerous commands) +- Permission override (deny/allow decisions) +- Work tracking (session management) +- Tool lifecycle (pre/post processing) +- Shell environment injection +- Compaction intelligence (context preservation) + +**Layer 2 — Event Bus (passive, observe-only):** +- Session lifecycle (created, ended, error, compacted, updated) +- Message processing (ISC validation, voice, ratings, sentiment) +- Permission audit logging +- Command tracking + +**Key Principle:** Plugins run asynchronously and fail gracefully. They enhance the user experience but never block OpenCode's core functionality. All logging uses `file-logger.ts` — NEVER `console.log` (corrupts TUI). + +--- + +## Claude Code → OpenCode Hook Mapping + +PAI-OpenCode translates Claude Code hook concepts to OpenCode plugin hooks: + +| PAI Hook (Claude Code) | OpenCode Plugin Hook | Mechanism | +|------------------------|---------------------|-------------| +| SessionStart | `experimental.chat.system.transform` | `output.system.push()` | +| PreToolUse | `tool.execute.before` | `throw Error()` to block | +| PreToolUse (blocking) | `permission.ask` | `output.status = "deny"` | +| PostToolUse | `tool.execute.after` | Read-only observation | +| UserPromptSubmit | `chat.message` | Filter `role === "user"` | +| Stop | `event` | Filter `session.ended` | +| SubagentStop | `tool.execute.after` | Filter `tool === "Task"` | +| — (new in OpenCode) | `experimental.session.compacting` | Context injection during compaction | +| — (new in OpenCode) | `shell.env` | Environment variable injection | +| — (new in OpenCode) | `tool` (custom tools) | Register `session_registry`, `session_results`, `code_review` | + +**Reference Implementation:** `plugins/pai-unified.ts` +**Type Definitions:** `plugins/adapters/types.ts` (includes `PAI_TO_OPENCODE_HOOKS` mapping) + +--- + +## Available Plugin Hooks (9 Active) + +### 1. `experimental.chat.system.transform` (SessionStart) + +**When:** At the start of each chat/session +**Purpose:** Inject minimal bootstrap context (~15KB) into the conversation + +Loads `MINIMAL_BOOTSTRAP.md` (core Algorithm + Steering Rules), System `AISTEERINGRULES.md`, and User identity files (ABOUTME, TELOS, DAIDENTITY). Skills load on-demand via OpenCode's native skill tool — no full 233KB context dump. + +**Emits:** `session.start`, `context.loaded` + +--- + +### 2. `permission.ask` (Security Blocking) + +**When:** When OpenCode asks for permission on a tool +**Purpose:** Override permission decisions — deny dangerous, confirm risky + +**Handler:** `security-validator.ts` +**Actions:** `block` → `output.status = "deny"` | `confirm` → `output.status = "ask"` | `allow` → no change + +**Note:** Not reliably called for all tools, so security validation also runs in `tool.execute.before`. + +**Emits:** `security.block`, `security.warn` + +--- + +### 3. `tool.execute.before` (PreToolUse) + +**When:** Before every tool execution +**Purpose:** Security validation + agent execution guard + skill guard + +**Handlers:** +- `security-validator.ts` — Validates Bash commands against dangerous/warning patterns. **Throws error to block.** +- `agent-execution-guard.ts` — Warns when agents spawn without proper capability selection (non-blocking) +- `skill-guard.ts` — Validates skill invocations match USE WHEN triggers (non-blocking) + +**Emits:** `security.block`, `security.warn` + +--- + +### 4. `tool.execute.after` (PostToolUse) + +**When:** After tool execution completes +**Purpose:** Capture outputs, track state, sync PRDs, track questions + +**Handlers:** +- `agent-capture.ts` — Captures subagent (Task tool) outputs to `MEMORY/RESEARCH/` +- `algorithm-tracker.ts` — Tracks Algorithm phase, validates transitions, tracks ISC criteria and agent spawns +- `prd-sync.ts` — When a PRD.md is written/edited, syncs frontmatter to `work-registry.json` for dashboard +- `question-tracking.ts` — Records AskUserQuestion Q&A pairs to `MEMORY/STATE/questions.jsonl` +- `session-registry.ts` — Captures subagent session IDs to registry for compaction recovery + +**Emits:** `tool.execute`, `agent.complete` + +--- + +### 5. `chat.message` (UserPromptSubmit) + +**When:** When user submits a message +**Purpose:** Work session creation, rating capture, effort level detection + +**Handlers:** +- `work-tracker.ts` — Creates work sessions in `MEMORY/WORK/` on first non-trivial user message, appends to thread +- `rating-capture.ts` — Detects explicit ratings (1-10) and persists to `MEMORY/LEARNING/SIGNALS/ratings.jsonl` +- `format-reminder.ts` — Detects effort level from user prompts using 8-tier system (Instant→Loop) + +**Features:** +- Message deduplication cache (5s TTL) prevents double-processing between `chat.message` and `message.updated` +- Trivial message detection (greetings, ratings, acknowledgments) skips work session creation +- Session-scoped message buffers for relationship memory analysis + +**Emits:** `user.message` + +--- + +### 6. `event` (Session Lifecycle + Message Processing) + +**When:** All session lifecycle events and message updates +**Purpose:** Orchestrates 15+ handlers across session start, end, and message processing + +#### Session Start (`session.created`) +- `skill-restore.ts` — Restores SKILL.md files modified by OpenCode's normalization +- `check-version.ts` — Checks for PAI-OpenCode updates via GitHub releases + +#### Session End (`session.ended` / `session.idle`) +- `learning-capture.ts` — Extracts learnings from work session, bridges `MEMORY/WORK/` to `MEMORY/LEARNING/` +- `integrity-check.ts` — Validates system health (required files, configs, MEMORY dirs, plugins) +- `work-tracker.ts` — Completes work session +- `update-counts.ts` — Updates `settings.json` with fresh system counts for banner/statusline +- `session-cleanup.ts` — Marks work directory COMPLETED, clears `current-work.json`, cleans `session-names.json` +- `relationship-memory.ts` — Analyzes session messages to extract relationship notes (W/B/O types) to `MEMORY/RELATIONSHIP/` + +#### Assistant Messages (`message.updated`, role=assistant) +- `isc-validator.ts` — Validates Algorithm format, counts ISC criteria, warns on missing elements +- `voice-notification.ts` — Extracts 🗣️ voice line, sends to TTS service (ElevenLabs or Google Cloud) +- `tab-state.ts` — Updates Kitty terminal tab title with 3-5 word completion summary +- `response-capture.ts` — Captures responses for work tracking, extracts ISC to `ISC.json` +- `last-response-cache.ts` — Caches last assistant response to `MEMORY/STATE/` for implicit sentiment context + +#### User Messages (`message.updated`, role=user) +- `rating-capture.ts` — Explicit rating detection (backup path via event bus) +- `implicit-sentiment.ts` — AI-powered sentiment analysis (1-10 scale) when no explicit rating given +- `work-tracker.ts` — Auto-work creation (backup path via event bus) + +#### Session Compacted (`session.compacted`) +- `learning-capture.ts` — Rescues learnings after compaction (POST-compaction, complementary to PRE-compaction hook) + +#### Other Events +- `session.error` — Error diagnostics logging +- `session.updated` — Session title tracking +- `permission.asked` — Full permission audit log +- `command.executed` — `/command` usage tracking +- `installation.update.available` — Native OpenCode update notification + +**Emits:** `session.start`, `session.end`, `assistant.message`, `explicit.rating`, `implicit.sentiment`, `isc.validated`, `voice.sent`, `learning.captured` + +--- + +### 7. `experimental.session.compacting` (Compaction Intelligence) + +**When:** During LLM summary generation for context compaction +**Purpose:** Inject PAI-critical context so the compaction summary preserves it + +**Handler:** `compaction-intelligence.ts` — Reads active PRD status, subagent registry, and ISC criteria, then appends to `output.context` so the LLM includes them in the compaction summary. + +**Complements:** `session.compacted` event (post-compaction learning rescue). Both are needed: one influences WHAT the LLM summarizes, the other rescues data AFTER compaction. + +--- + +### 8. `tool` (Custom Tools) + +**When:** Always available — registers custom tools for the AI to call +**Purpose:** Provide session recovery and code review tools + +**Tools registered:** +- `session_registry` — Lists all subagent sessions with metadata for the current session (compaction recovery) +- `session_results` — Gets registry metadata for a specific subagent + resume instructions +- `code_review` — Runs roborev for AI-powered code review (dirty, last-commit, fix, refine modes) + +**Handlers:** `session-registry.ts`, `roborev-trigger.ts` + +--- + +### 9. `shell.env` (Shell Environment Injection) + +**When:** Before every Bash tool call +**Purpose:** Inject PAI runtime context into stateless shell processes + +OpenCode Bash is **stateless** — every call spawns a fresh process. This hook injects: +- `PAI_CONTEXT=1`, `PAI_SESSION_ID`, `PAI_WORK_DIR`, `PAI_VERSION` +- Explicit passthrough of keys: `PAI_OBSERVABILITY_PORT`, `GOOGLE_API_KEY`, `TTS_PROVIDER`, `DA`, `TIME_ZONE` + +**Two-layer system:** Layer 1 (`.opencode/.env` loaded by Bun) handles API keys in TypeScript. Layer 2 (this hook) handles Bash child processes needing runtime context. + +--- + +## Handler Reference (27 Handlers) + +| Handler | Hook | Purpose | +|---------|------|---------| +| `agent-capture.ts` | tool.execute.after | Captures subagent outputs to MEMORY/RESEARCH/ | +| `agent-execution-guard.ts` | tool.execute.before | Validates agent spawning patterns (non-blocking) | +| `algorithm-tracker.ts` | tool.execute.after | Tracks Algorithm phase, ISC criteria, agent spawns | +| `check-version.ts` | event (session.created) | Checks for PAI-OpenCode updates via GitHub | +| `compaction-intelligence.ts` | experimental.session.compacting | Injects PRD/ISC/registry into compaction summary | +| `format-reminder.ts` | chat.message | Detects effort level (8-tier: Instant→Loop) | +| `implicit-sentiment.ts` | event (message.updated) | AI sentiment analysis on user messages (1-10) | +| `integrity-check.ts` | event (session.ended) | System health validation (files, configs, MEMORY) | +| `isc-validator.ts` | event (message.updated) | Validates Algorithm format, counts ISC criteria | +| `last-response-cache.ts` | event (message.updated) | Caches last response for sentiment context | +| `learning-capture.ts` | event (session.ended, compacted) | Extracts learnings, bridges WORK→LEARNING | +| `observability-emitter.ts` | (all hooks) | Fire-and-forget event emission to observability server | +| `prd-sync.ts` | tool.execute.after | Syncs PRD frontmatter to work-registry.json | +| `question-tracking.ts` | tool.execute.after | Records AskUserQuestion Q&A pairs | +| `rating-capture.ts` | chat.message, event | Detects explicit ratings (1-10) | +| `relationship-memory.ts` | event (session.ended) | Extracts relationship notes (W/B/O types) | +| `response-capture.ts` | event (message.updated) | Captures responses, extracts ISC to ISC.json | +| `roborev-trigger.ts` | tool (custom) | AI code review via roborev CLI | +| `security-validator.ts` | permission.ask, tool.execute.before | Pattern-based security validation (block/confirm/allow) | +| `session-cleanup.ts` | event (session.ended) | Marks COMPLETED, clears state, cleans session-names | +| `session-registry.ts` | tool (custom), tool.execute.after | Tracks subagent sessions for compaction recovery | +| `skill-guard.ts` | tool.execute.before | Validates skill invocations match triggers | +| `skill-restore.ts` | event (session.created) | Restores SKILL.md files modified by OpenCode | +| `tab-state.ts` | event (message.updated) | Updates Kitty terminal tab title/color | +| `update-counts.ts` | event (session.ended) | Refreshes settings.json system counts | +| `voice-notification.ts` | event (message.updated) | Sends 🗣️ voice line to TTS service | +| `work-tracker.ts` | chat.message, event | Creates/manages work sessions in MEMORY/WORK/ | + +--- + +## Library Reference (8 Libraries) + +| Library | Purpose | +|---------|---------| +| `file-logger.ts` | TUI-safe file logging — NEVER use `console.log` in plugins | +| `paths.ts` | Canonical path construction for MEMORY, WORK, LEARNING directories | +| `identity.ts` | Central identity loader (DA name, Principal name from settings.json) | +| `time.ts` | Consistent timestamp formatting (ISO, PST/PDT) | +| `sanitizer.ts` | Input normalization before security pattern matching (base64 decode, Unicode, spacing) | +| `injection-patterns.ts` | Comprehensive prompt injection pattern library (7 categories) | +| `learning-utils.ts` | Learning categorization (SYSTEM vs ALGORITHM) shared across handlers | +| `model-config.ts` | PAI model configuration schema and ZEN provider definitions | +| `db-utils.ts` | Database health checks, size monitoring, session archiving | + +--- + +## Plugin Architecture + +```text +plugins/ +├── pai-unified.ts # Main plugin — routes all 9 hooks to handlers +├── handlers/ # 27 specialized handler modules +│ ├── agent-capture.ts # Subagent output capture +│ ├── agent-execution-guard.ts # Agent spawning validation +│ ├── algorithm-tracker.ts # Algorithm phase tracking +│ ├── check-version.ts # Update checking +│ ├── compaction-intelligence.ts # Compaction context injection +│ ├── format-reminder.ts # Effort level detection +│ ├── implicit-sentiment.ts # AI sentiment analysis +│ ├── integrity-check.ts # System health checks +│ ├── isc-validator.ts # ISC format validation +│ ├── last-response-cache.ts # Response caching for sentiment +│ ├── learning-capture.ts # Learning extraction +│ ├── observability-emitter.ts # Event emission (fire-and-forget) +│ ├── prd-sync.ts # PRD frontmatter sync +│ ├── question-tracking.ts # Q&A pair tracking +│ ├── rating-capture.ts # Explicit rating detection +│ ├── relationship-memory.ts # Session relationship notes +│ ├── response-capture.ts # Response capture + ISC extraction +│ ├── roborev-trigger.ts # AI code review tool +│ ├── security-validator.ts # Security pattern matching +│ ├── session-cleanup.ts # Session finalization +│ ├── session-registry.ts # Subagent session tracking +│ ├── skill-guard.ts # Skill invocation validation +│ ├── skill-restore.ts # SKILL.md git restore +│ ├── tab-state.ts # Terminal tab management +│ ├── update-counts.ts # Settings.json count refresh +│ ├── voice-notification.ts # TTS voice output +│ └── work-tracker.ts # Work session management +├── adapters/ +│ └── types.ts # Shared types + PAI_TO_OPENCODE_HOOKS mapping +└── lib/ # 9 shared libraries + ├── db-utils.ts # Database health + ├── file-logger.ts # TUI-safe logging + ├── identity.ts # DA/Principal identity + ├── injection-patterns.ts # Security patterns (7 categories) + ├── learning-utils.ts # Learning categorization + ├── model-config.ts # Model/provider config + ├── paths.ts # Path utilities + ├── sanitizer.ts # Input normalization + └── time.ts # Timestamp formatting +``` + +**Key Design Decisions:** + +1. **Single Plugin File** — `pai-unified.ts` exports all hooks from one plugin (OpenCode auto-discovers it) +2. **Handler Separation** — Complex logic in `handlers/` for maintainability and testability +3. **File Logging** — Never use `console.log` (corrupts OpenCode TUI), use `file-logger.ts` +4. **Fail-Open Security** — On handler error, don't block (avoid hanging OpenCode) +5. **Message Deduplication** — 5s cache prevents double-processing between `chat.message` and `message.updated` +6. **Session-Scoped Buffers** — Message buffers keyed by sessionId prevent cross-session contamination +7. **Two-Layer Compaction** — `experimental.session.compacting` (PRE) + `session.compacted` event (POST) + +--- + +## Configuration + +### Plugin Registration (Auto-Discovery) + +OpenCode **automatically discovers** plugins from the `plugins/` directory — **no config entry needed!** + +```text +.opencode/ + plugins/ + pai-unified.ts # Auto-discovered and loaded +``` + +OpenCode scans `{plugin,plugins}/*.{ts,js}` and loads all matching files automatically. + +**Important:** Do NOT add relative paths to `opencode.json` — this causes `BunInstallFailedError`. + +If you must explicitly register a plugin (e.g., from npm or absolute path), use: + +```json +{ + "plugin": [ + "some-npm-package", + "file:///absolute/path/to/plugin.ts" + ] +} +``` + +**Note:** The config key is `plugin` (singular), not `plugins` (plural). + +### Identity Configuration + +PAI-specific identity configuration is handled via: +- `USER/DAIDENTITY.md` → AI personality and voice settings +- `USER/TELOS/` → User context, goals, and preferences +- `opencode.json` → `username` field + +--- + +## Security Patterns + +Security validation uses multi-layer pattern matching against dangerous commands: + +**Blocked Patterns (DANGEROUS_PATTERNS):** +- `rm -rf /` — Root-level deletion +- `rm -rf ~/` — Home directory deletion +- `mkfs.` — Filesystem formatting +- `bash -i >&` — Reverse shells +- `curl | bash` — Remote code execution +- `cat .ssh/id_` — Credential theft +- `eval $(echo ... | base64 -d)` — Obfuscated RCE +- `printenv | curl` — Environment exfiltration +- `python -c "import os; os.system()"` — Python RCE one-liners +- `node -e "require('child_process')"` — Node RCE one-liners + +**Warning Patterns (WARNING_PATTERNS):** +- `git push --force` — Force push +- `git reset --hard` — Hard reset +- `npm install -g` — Global installs +- `docker rm` — Container removal + +**Enhanced in v3.0 (WP-B):** +- 7-category injection pattern detection (`injection-patterns.ts`) +- Input sanitization before matching (`sanitizer.ts` — base64 decode, Unicode normalization) +- Security audit logging to `security-audit.jsonl` +- Multi-field scanning (not just `args.content`) + +See `plugins/adapters/types.ts` for full pattern definitions. + +--- + +## Logging + +**CRITICAL:** Never use `console.log` in plugins — it corrupts the OpenCode TUI. + +Use the file logger instead: + +```typescript +import { fileLog, fileLogError, clearLog } from "./lib/file-logger"; + +fileLog("Plugin loaded"); +fileLog("Warning message", "warn"); +fileLogError("Something failed", error); +``` + +Log file location: `~/.opencode/plugins/debug.log` + +--- + +## Observability + +The `observability-emitter.ts` handler sends events to the PAI Observability Server for real-time monitoring: + +**Design:** Fire-and-forget with 1-second timeout. Server unavailability is not an error. + +**Events emitted:** +- `session.start`, `session.end` +- `context.loaded` +- `user.message`, `assistant.message` +- `tool.execute`, `agent.complete` +- `security.block`, `security.warn` +- `explicit.rating`, `implicit.sentiment` +- `isc.validated` +- `voice.sent` +- `learning.captured` + +Configure via `PAI_OBSERVABILITY_PORT` and `PAI_OBSERVABILITY_ENABLED` environment variables. + +--- + +## Troubleshooting + +### Plugin Not Loading + +1. Is the plugin file in `.opencode/plugins/`? (Auto-discovery location) +2. Can Bun parse the TypeScript? `bun run .opencode/plugins/pai-unified.ts` +3. Are there TypeScript errors? Check `~/.opencode/plugins/debug.log` +4. If using `opencode.json`: Use `plugin` (singular), not `plugins` (plural) +5. If using explicit paths: Use `file://` URL format, not relative paths + +### Context Not Injecting + +1. Does `MINIMAL_BOOTSTRAP.md` exist in `.opencode/PAI/`? +2. Check `~/.opencode/plugins/debug.log` for loading errors +3. Verify bootstrap loader can find PAI skill directory + +### Security Blocking Everything + +1. Review `debug.log` for which pattern matched +2. Verify command is actually safe +3. Check for false positives in pattern matching +4. Review `security-audit.jsonl` for audit trail + +### TUI Corruption + +**Cause:** Using `console.log` in plugin code +**Fix:** Replace all `console.log` with `fileLog` from `lib/file-logger.ts` + +--- + +## Migration from Claude Code Hooks + +If migrating from PAI's Claude Code implementation: + +| Claude Code | OpenCode | Notes | +|-------------|----------|-------| +| `hooks/` directory | `plugins/` directory | Different location | +| `settings.json` hooks | `opencode.json` plugins | Different config | +| Exit code 2 to block | `throw Error()` | Different mechanism | +| Reads stdin for input | Function parameters | Different API | +| Multiple hook files | Single unified plugin | Recommended pattern | +| No custom tools | `tool` hook | New: register custom tools | +| No compaction hook | `experimental.session.compacting` | New: influence compaction | +| No shell env hook | `shell.env` | New: inject env vars | + +**Key Differences:** +1. OpenCode plugins use async functions, not external scripts +2. Blocking uses `throw Error()` instead of `exit(2)` +3. Input comes from function parameters, not stdin +4. All hooks can be combined in one plugin file +5. Three new hooks available (custom tools, compaction, shell.env) + +--- + +## Related Documentation + +- **Memory System:** `SYSTEM/MEMORYSYSTEM.md` +- **Agent System:** `SYSTEM/PAIAGENTSYSTEM.md` +- **Architecture:** `SYSTEM/PAISYSTEMARCHITECTURE.md` +- **Security Patterns:** `plugins/adapters/types.ts` +- **Observability:** `plugins/handlers/observability-emitter.ts` + +--- + +**Last Updated:** 2026-03-17 +**Status:** Production — 27 handlers, 9 libraries, 9 hooks active +**Maintainer:** PAI System diff --git a/.opencode/PAI/Tools/GenerateSkillIndex.ts b/.opencode/PAI/Tools/GenerateSkillIndex.ts new file mode 100644 index 00000000..5f230f0e --- /dev/null +++ b/.opencode/PAI/Tools/GenerateSkillIndex.ts @@ -0,0 +1,374 @@ +#!/usr/bin/env bun +/** + * GenerateSkillIndex.ts + * + * Parses all SKILL.md files and builds a searchable index for dynamic skill discovery. + * Run this after adding/modifying skills to update the index. + * + * Usage: bun run ~/.opencode/skills/PAI/Tools/GenerateSkillIndex.ts + * + * Output: ~/.opencode/skills/skill-index.json + */ + +import { readdir, readFile, writeFile, stat } from 'fs/promises'; +import { join, relative, sep } from 'path'; +import { existsSync } from 'fs'; + +const SKILLS_DIR = join(import.meta.dir, '..', '..', 'skills'); +const OUTPUT_FILE = join(SKILLS_DIR, 'skill-index.json'); + +interface SkillEntry { + name: string; + path: string; + category: string | null; // null for flat skills, category name for hierarchical + fullDescription: string; + triggers: string[]; + workflows: string[]; + tier: 'always' | 'deferred'; + isHierarchical: boolean; // true if in skills/Category/Skill/ structure +} + +interface SkillIndex { + generated: string; + totalSkills: number; + categories: number; + flatSkills: number; + hierarchicalSkills: number; + alwaysLoadedCount: number; + deferredCount: number; + skills: Record; + categoryMap: Record; // category -> skill names +} + +// Skills that should always be fully loaded (Tier 1) +const ALWAYS_LOADED_SKILLS = [ + 'CORE', + 'Development', + 'Research', + 'Blogging', + 'Art', +]; + +async function findSkillFiles(dir: string): Promise { + const skillFiles: string[] = []; + + try { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + // Follow symlinks to directories (upstream 1d2fcb5) + let isDirectory = entry.isDirectory(); + if (entry.isSymbolicLink()) { + try { + const stats = await stat(fullPath); + isDirectory = stats.isDirectory(); + } catch { + // Broken symlink — skip silently + continue; + } + } + + if (isDirectory) { + // Skip hidden directories and node_modules + if (entry.name.startsWith('.') || entry.name === 'node_modules') { + continue; + } + + // Check for SKILL.md in this directory + const skillMdPath = join(fullPath, 'SKILL.md'); + if (existsSync(skillMdPath)) { + skillFiles.push(skillMdPath); + } + + // Recurse into subdirectories (including symlinked ones) + const nestedFiles = await findSkillFiles(fullPath); + skillFiles.push(...nestedFiles); + } + } + } catch (error) { + console.error(`Error scanning directory ${dir}:`, error); + } + + return skillFiles; +} + +function parseFrontmatter(content: string): { name: string; description: string } | null { + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); + if (!frontmatterMatch) return null; + + const frontmatter = frontmatterMatch[1]; + + // Extract name + const nameMatch = frontmatter.match(/^name:\s*(.+)$/m); + const name = nameMatch ? nameMatch[1].trim() : ''; + + // Extract description (handles both single-line and multi-line YAML with | or >) + let description = ''; + + // Find the description line + const descLineMatch = frontmatter.match(/^description:\s*(.*)$/m); + if (descLineMatch) { + const indicator = descLineMatch[1].trim(); // |, >, |-, >- or empty + + if (indicator === '|' || indicator === '>' || indicator === '|-' || indicator === '>-') { + // Multiline YAML - extract content until next field + const descStart = frontmatter.indexOf(descLineMatch[0]) + descLineMatch[0].length; + const restOfFrontmatter = frontmatter.slice(descStart); + + // Find where next field starts (line beginning with field name:) + const nextFieldMatch = restOfFrontmatter.match(/\n([0-9A-Za-z_-]+):/); + const rawDesc = nextFieldMatch + ? restOfFrontmatter.slice(0, nextFieldMatch.index) + : restOfFrontmatter; + + if (indicator === '>' || indicator === '>-') { + // Folded style: newlines become spaces + description = rawDesc + .split('\n') + .map(line => line.trimStart()) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + } else { + // Literal style (| or |-): preserve content but remove common indentation + const lines = rawDesc.split('\n').filter(l => l.trim().length > 0); + if (lines.length > 0) { + const minIndent = lines.reduce((min, line) => { + const match = line.match(/^(\s*)/); + const indent = match ? match[1].length : 0; + return Math.min(min, indent); + }, Infinity); + description = lines + .map(line => line.slice(minIndent)) + .join('\n') + .trim(); + } + } + } else { + // Single-line description + description = indicator; + } + } + + return { name, description }; +} + +function extractTriggers(description: string): string[] { + const triggers: string[] = []; + + // Extract from USE WHEN patterns + const useWhenMatch = description.match(/USE WHEN[^.]+/gi); + if (useWhenMatch) { + for (const match of useWhenMatch) { + // Extract quoted phrases and keywords + const words = match + .replace(/USE WHEN/gi, '') + .replace(/user (says|wants|mentions|asks)/gi, '') + .replace(/['"]/g, '') + .split(/[,\s]+/) + .map(w => w.toLowerCase().trim()) + .filter(w => w.length > 2 && !['the', 'and', 'for', 'with', 'from', 'about'].includes(w)); + + triggers.push(...words); + } + } + + // Also extract key terms from the description + const keyTerms = description + .toLowerCase() + .match(/\b(scrape|parse|extract|research|blog|art|visual|mcp|osint|newsletter|voice|browser|automation|security|vuln|recon|upgrade|telos|gmail|youtube|clickup|cloudflare|lifelog|headshot|council|eval|fabric|dotfiles)\b/g); + + if (keyTerms) { + triggers.push(...keyTerms); + } + + // Deduplicate + return [...new Set(triggers)]; +} + +function extractWorkflows(content: string): string[] { + const workflows: string[] = []; + + // Look for workflow routing section + const workflowMatches = content.matchAll(/[-*]\s*\*\*([A-Z][A-Z_]+)\*\*|→\s*`Workflows\/([^`]+)\.md`|[-*]\s*([A-Za-z]+)\s*→\s*`/g); + + for (const match of workflowMatches) { + const workflow = match[1] || match[2] || match[3]; + if (workflow) { + workflows.push(workflow); + } + } + + // Also check for workflow files mentioned + const workflowFileMatches = content.matchAll(/Workflows?\/([A-Za-z]+)\.md/g); + for (const match of workflowFileMatches) { + if (match[1] && !workflows.includes(match[1])) { + workflows.push(match[1]); + } + } + + return [...new Set(workflows)]; +} + +async function parseSkillFile(filePath: string): Promise { + try { + const content = await readFile(filePath, 'utf-8'); + const frontmatter = parseFrontmatter(content); + + if (!frontmatter || !frontmatter.name) { + console.warn(`Skipping ${filePath}: No valid frontmatter`); + return null; + } + + const triggers = extractTriggers(frontmatter.description); + const workflows = extractWorkflows(content); + const tier = ALWAYS_LOADED_SKILLS.includes(frontmatter.name) ? 'always' : 'deferred'; + + // Determine category from path (cross-platform using path.relative and path.sep) + const relPath = relative(SKILLS_DIR, filePath); + const pathParts = relPath.split(sep).filter(p => p !== ''); + + // Hierarchical structure: Category/Skill/SKILL.md (3 parts) + // Flat structure: Skill/SKILL.md (2 parts) + // Deeper nesting (>3 parts) is warned but still treated as hierarchical + if (pathParts.length > 3) { + console.warn(`⚠️ Deep nesting detected at ${filePath} (${pathParts.length} levels). Only 2 levels (Category/Skill) are supported.`); + } + + const isHierarchical = pathParts.length >= 3; + const category = isHierarchical ? pathParts[0] : null; + const relativePath = relPath.replace(/\\/g, '/'); // Normalize to forward slashes for output + + return { + name: frontmatter.name, + path: relativePath, + category, + fullDescription: frontmatter.description, + triggers, + workflows, + tier, + isHierarchical, + }; + } catch (error) { + console.error(`Error parsing ${filePath}:`, error); + return null; + } +} + +async function main() { + console.log('🔍 Generating skill index for hierarchical structure...\n'); + + const skillFiles = await findSkillFiles(SKILLS_DIR); + console.log(`Found ${skillFiles.length} SKILL.md files\n`); + + const index: SkillIndex = { + generated: new Date().toISOString(), + totalSkills: 0, + categories: 0, + flatSkills: 0, + hierarchicalSkills: 0, + alwaysLoadedCount: 0, + deferredCount: 0, + skills: {}, + categoryMap: {}, + }; + + // Track categories + const categories = new Set(); + + // Sort skillFiles deterministically + skillFiles.sort((a, b) => a.localeCompare(b)); + + for (const filePath of skillFiles) { + const skill = await parseSkillFile(filePath); + if (skill) { + const key = skill.name.toLowerCase(); + + // Check for duplicates - don't overwrite existing entries + if (index.skills[key]) { + console.warn(`⚠️ Duplicate skill name "${skill.name}" found at ${skill.path} (existing: ${index.skills[key].path})`); + // Skip adding duplicate + continue; + } + + index.skills[key] = skill; + index.totalSkills++; + + if (skill.tier === 'always') { + index.alwaysLoadedCount++; + } else { + index.deferredCount++; + } + + if (skill.isHierarchical) { + index.hierarchicalSkills++; + if (skill.category) { + categories.add(skill.category); + if (!index.categoryMap[skill.category]) { + index.categoryMap[skill.category] = []; + } + index.categoryMap[skill.category].push(skill.name); + } + } else { + index.flatSkills++; + } + + const icon = skill.tier === 'always' ? '🔒' : '📦'; + const structure = skill.isHierarchical ? `📁 ${skill.category}/` : '📄 flat'; + console.log(` ${icon} ${structure} ${skill.name}: ${skill.triggers.length} triggers, ${skill.workflows.length} workflows`); + } + } + + index.categories = categories.size; + + // Sort categoryMap entries deterministically + for (const category of Object.keys(index.categoryMap)) { + index.categoryMap[category].sort((a, b) => a.localeCompare(b)); + } + + // Create sorted skills object for deterministic output + const sortedSkills: Record = {}; + for (const key of Object.keys(index.skills).sort((a, b) => a.localeCompare(b))) { + sortedSkills[key] = index.skills[key]; + } + index.skills = sortedSkills; + + // Write the index + await writeFile(OUTPUT_FILE, JSON.stringify(index, null, 2)); + + console.log(`\n✅ Index generated: ${OUTPUT_FILE}`); + console.log(`\n📊 Structure Overview:`); + console.log(` Total Skills: ${index.totalSkills}`); + console.log(` 📁 Categories: ${index.categories}`); + console.log(` 📄 Flat Skills: ${index.flatSkills}`); + console.log(` 📁 Hierarchical: ${index.hierarchicalSkills}`); + console.log(`\n⚡ Loading Strategy:`); + console.log(` Always Loaded: ${index.alwaysLoadedCount}`); + console.log(` Deferred: ${index.deferredCount}`); + + // Calculate token estimates + const avgFullTokens = 150; + const avgMinimalTokens = 25; + const currentTokens = index.totalSkills * avgFullTokens; + const newTokens = (index.alwaysLoadedCount * avgFullTokens) + (index.deferredCount * avgMinimalTokens); + const savings = ((currentTokens - newTokens) / currentTokens * 100).toFixed(1); + + console.log(`\n💰 Estimated token impact:`); + console.log(` Current: ~${currentTokens.toLocaleString()} tokens`); + console.log(` After: ~${newTokens.toLocaleString()} tokens`); + console.log(` Savings: ~${savings}%`); + + // Show category breakdown (sorted) + if (index.categories > 0) { + console.log(`\n📂 Category Breakdown:`); + const sortedCategories = Object.keys(index.categoryMap).sort((a, b) => a.localeCompare(b)); + for (const category of sortedCategories) { + const skills = index.categoryMap[category]; + console.log(` ${category}: ${skills.length} skills`); + } + } +} + +main().catch(console.error); diff --git a/.opencode/PAI/Tools/SkillSearch.ts b/.opencode/PAI/Tools/SkillSearch.ts new file mode 100644 index 00000000..2914852f --- /dev/null +++ b/.opencode/PAI/Tools/SkillSearch.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env bun +/** + * SkillSearch.ts + * + * Search the skill index to discover capabilities dynamically. + * Use this when you need to find which skill handles a specific task. + * + * Usage: + * bun run ~/.opencode/skills/PAI/Tools/SkillSearch.ts + * bun run ~/.opencode/skills/PAI/Tools/SkillSearch.ts "scrape instagram" + * bun run ~/.opencode/skills/PAI/Tools/SkillSearch.ts --list # List all skills + * bun run ~/.opencode/skills/PAI/Tools/SkillSearch.ts --tier always # List always-loaded skills + * bun run ~/.opencode/skills/PAI/Tools/SkillSearch.ts --tier deferred # List deferred skills + * + * Output: Matching skills with full descriptions and workflows + */ + +import { readFile } from 'fs/promises'; +import { join } from 'path'; +import { existsSync } from 'fs'; + +const INDEX_FILE = join(import.meta.dir, '..', 'Skills', 'skill-index.json'); + +interface SkillEntry { + name: string; + path: string; + fullDescription: string; + triggers: string[]; + workflows: string[]; + tier: 'always' | 'deferred'; +} + +interface SkillIndex { + generated: string; + totalSkills: number; + alwaysLoadedCount: number; + deferredCount: number; + skills: Record; +} + +interface SearchResult { + skill: SkillEntry; + score: number; + matchedTriggers: string[]; +} + +function searchSkills(query: string, index: SkillIndex): SearchResult[] { + const queryTerms = query.toLowerCase().split(/\s+/).filter(t => t.length > 1); + const results: SearchResult[] = []; + + for (const [key, skill] of Object.entries(index.skills)) { + let score = 0; + const matchedTriggers: string[] = []; + + // Check skill name + if (key.includes(query.toLowerCase()) || skill.name.toLowerCase().includes(query.toLowerCase())) { + score += 10; + } + + // Check triggers + for (const term of queryTerms) { + for (const trigger of skill.triggers) { + if (trigger.includes(term) || term.includes(trigger)) { + score += 5; + if (!matchedTriggers.includes(trigger)) { + matchedTriggers.push(trigger); + } + } + } + } + + // Check description + const descLower = skill.fullDescription.toLowerCase(); + for (const term of queryTerms) { + if (descLower.includes(term)) { + score += 2; + } + } + + // Check workflows + for (const workflow of skill.workflows) { + for (const term of queryTerms) { + if (workflow.toLowerCase().includes(term)) { + score += 3; + } + } + } + + if (score > 0) { + results.push({ skill, score, matchedTriggers }); + } + } + + // Sort by score descending + return results.sort((a, b) => b.score - a.score); +} + +function formatResult(result: SearchResult): string { + const { skill, score, matchedTriggers } = result; + const tierIcon = skill.tier === 'always' ? '🔒' : '📦'; + + let output = `\n${'─'.repeat(60)}\n`; + output += `${tierIcon} **${skill.name}** (score: ${score})\n`; + output += `${'─'.repeat(60)}\n\n`; + + output += `**Path:** ${skill.path}\n`; + output += `**Tier:** ${skill.tier}\n\n`; + + output += `**Description:**\n${skill.fullDescription}\n\n`; + + if (matchedTriggers.length > 0) { + output += `**Matched Triggers:** ${matchedTriggers.join(', ')}\n\n`; + } + + if (skill.workflows.length > 0) { + output += `**Workflows:** ${skill.workflows.join(', ')}\n\n`; + } + + output += `**Invoke with:** Skill { skill: "${skill.name}" }\n`; + + return output; +} + +function listSkills(index: SkillIndex, tier?: 'always' | 'deferred'): void { + console.log(`\n📚 Skill Index (generated: ${index.generated})\n`); + + const skills = Object.values(index.skills) + .filter(s => !tier || s.tier === tier) + .sort((a, b) => a.name.localeCompare(b.name)); + + if (tier === 'always') { + console.log('🔒 Always-Loaded Skills (full descriptions in context):\n'); + } else if (tier === 'deferred') { + console.log('📦 Deferred Skills (minimal descriptions, search for details):\n'); + } else { + console.log('All Skills:\n'); + } + + for (const skill of skills) { + const tierIcon = skill.tier === 'always' ? '🔒' : '📦'; + const triggerPreview = skill.triggers.slice(0, 5).join(', '); + console.log(` ${tierIcon} ${skill.name.padEnd(20)} │ ${triggerPreview}`); + } + + console.log(`\nTotal: ${skills.length} skills`); +} + +async function main() { + // Check if index exists + if (!existsSync(INDEX_FILE)) { + console.error('❌ Skill index not found. Run GenerateSkillIndex.ts first:'); + console.error(' bun run ~/.opencode/skills/PAI/Tools/GenerateSkillIndex.ts'); + process.exit(1); + } + + const indexContent = await readFile(INDEX_FILE, 'utf-8'); + const index: SkillIndex = JSON.parse(indexContent); + + const args = process.argv.slice(2); + + // Handle flags + if (args.includes('--list') || args.length === 0) { + listSkills(index); + return; + } + + if (args.includes('--tier')) { + const tierIndex = args.indexOf('--tier'); + const tier = args[tierIndex + 1] as 'always' | 'deferred'; + if (tier === 'always' || tier === 'deferred') { + listSkills(index, tier); + } else { + console.error('Invalid tier. Use: always or deferred'); + } + return; + } + + // Search mode + const query = args.join(' '); + console.log(`\n🔍 Searching for: "${query}"\n`); + + const results = searchSkills(query, index); + + if (results.length === 0) { + console.log('No matching skills found.\n'); + console.log('Try broader terms or run with --list to see all skills.'); + return; + } + + // Show top 5 results + const topResults = results.slice(0, 5); + console.log(`Found ${results.length} matching skills. Showing top ${topResults.length}:\n`); + + for (const result of topResults) { + console.log(formatResult(result)); + } + + if (results.length > 5) { + console.log(`\n... and ${results.length - 5} more results.`); + } +} + +main().catch(console.error); diff --git a/.opencode/PAI/Tools/ValidateSkillStructure.ts b/.opencode/PAI/Tools/ValidateSkillStructure.ts new file mode 100644 index 00000000..ffc9d067 --- /dev/null +++ b/.opencode/PAI/Tools/ValidateSkillStructure.ts @@ -0,0 +1,353 @@ +#!/usr/bin/env bun +/** + * ValidateSkillStructure.ts + * + * Validates the skill directory structure for consistency and correctness. + * Run this to check for common issues after reorganizing skills. + * + * Usage: bun run ~/.opencode/skills/PAI/Tools/ValidateSkillStructure.ts + * + * Checks: + * - All skills have valid SKILL.md with frontmatter + * - No orphaned skills (skills without parent category if in hierarchical structure) + * - Category SKILL.md files exist for all categories + * - No duplicate skill names + * - Path consistency + */ + +import { readdir, readFile, stat, realpath } from 'fs/promises'; +import { join, relative, sep } from 'path'; +import { existsSync } from 'fs'; + +const SKILLS_DIR = join(import.meta.dir, '..', '..', '..', 'skills'); + +interface ValidationIssue { + type: 'error' | 'warning'; + path: string; + message: string; +} + +interface ValidationResult { + valid: boolean; + issues: ValidationIssue[]; + stats: { + totalSkills: number; + categories: number; + flatSkills: number; + hierarchicalSkills: number; + errors: number; + warnings: number; + }; +} + +async function validateSkillStructure(): Promise { + const issues: ValidationIssue[] = []; + const skillNames = new Map(); // name -> path (for duplicates) + const categories = new Set(); + const reportedCategories = new Set(); // Track reported missing category SKILL.md + let flatSkills = 0; + let hierarchicalSkills = 0; + + async function scanDirectory(dir: string, depth: number = 0, visitedPaths: Set = new Set()): Promise { + try { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (entry.isSymbolicLink()) { + try { + const stats = await stat(fullPath); + if (!stats.isDirectory()) continue; + // Valid symlinked directory — check for cycles before recursing + const canonical = await realpath(fullPath); + if (visitedPaths.has(canonical)) { + issues.push({ + type: 'error', + path: fullPath, + message: `Symlink cycle detected: ${fullPath} -> ${canonical}`, + }); + continue; + } + // Will be processed below; canonical path added before recursion + } catch (err) { + // Report broken symlinks as structural errors + issues.push({ + type: 'error', + path: fullPath, + message: `Broken symlink: ${err instanceof Error ? err.message : String(err)}`, + }); + continue; + } + } + + // Determine if directory (including resolved symlinks) + const isDirectory = entry.isSymbolicLink() + ? (await stat(fullPath)).isDirectory() + : entry.isDirectory(); + + if (isDirectory) { + // Skip hidden and node_modules + if (entry.name.startsWith('.') || entry.name === 'node_modules') { + continue; + } + + const skillMdPath = join(fullPath, 'SKILL.md'); + + if (existsSync(skillMdPath)) { + // Found a skill + const relativePath = relative(SKILLS_DIR, fullPath); + const pathParts = relativePath.split(sep); + + if (pathParts.length === 1) { + // Flat skill: skills/SkillName/ + flatSkills++; + await validateSkill(skillMdPath, relativePath, issues, skillNames); + } else if (pathParts.length === 2) { + // Hierarchical skill: skills/Category/SkillName/ + hierarchicalSkills++; + categories.add(pathParts[0]); + await validateSkill(skillMdPath, relativePath, issues, skillNames); + + // Check if category SKILL.md exists (deduplicated reporting) + const categoryPath = join(SKILLS_DIR, pathParts[0]); + const categorySkillPath = join(categoryPath, 'SKILL.md'); + if (!existsSync(categorySkillPath) && !reportedCategories.has(pathParts[0])) { + reportedCategories.add(pathParts[0]); + issues.push({ + type: 'error', + path: categoryPath, + message: `Missing category SKILL.md for "${pathParts[0]}"`, + }); + } + } else if (pathParts.length > 2) { + // Too deep nesting + issues.push({ + type: 'error', + path: fullPath, + message: `Too deep nesting (${pathParts.length} levels). Max: 2 (Category/Skill)`, + }); + } + } else { + // No SKILL.md - might be a category or invalid + if (depth === 0) { + // Could be a category (allowed at top level without SKILL.md if it has subdirs) + await scanDirectory(fullPath, depth + 1, visitedPaths); + continue; // Prevent double recursion + } + } + + // Recurse for subdirectories (only if not already recursed above) + await scanDirectory(fullPath, depth + 1, visitedPaths); + } + } + } catch (error) { + issues.push({ + type: 'error', + path: dir, + message: `Failed to scan directory: ${error}`, + }); + } + } + + await scanDirectory(SKILLS_DIR); + + const errors = issues.filter(i => i.type === 'error').length; + const warnings = issues.filter(i => i.type === 'warning').length; + + return { + valid: errors === 0, + issues, + stats: { + totalSkills: flatSkills + hierarchicalSkills, + categories: categories.size, + flatSkills, + hierarchicalSkills, + errors, + warnings, + }, + }; +} + +async function validateSkill( + skillPath: string, + relativePath: string, + issues: ValidationIssue[], + skillNames: Map +): Promise { + try { + const content = await readFile(skillPath, 'utf-8'); + + // Check frontmatter + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!frontmatterMatch) { + issues.push({ + type: 'error', + path: relativePath, + message: 'Missing frontmatter (---)', + }); + return; + } + + const frontmatter = frontmatterMatch[1]; + + // Check name + const nameMatch = frontmatter.match(/^name:\s*(.+)$/m); + if (!nameMatch) { + issues.push({ + type: 'error', + path: relativePath, + message: 'Missing "name" in frontmatter', + }); + } else { + const name = nameMatch[1].trim(); + + // Check for duplicates + if (skillNames.has(name.toLowerCase())) { + issues.push({ + type: 'error', + path: relativePath, + message: `Duplicate skill name "${name}" (also at ${skillNames.get(name.toLowerCase())})`, + }); + } else { + skillNames.set(name.toLowerCase(), relativePath); + } + + // Check name matches directory name (best practice, not required) + const dirName = relativePath.split('/').pop(); + if (dirName && name.toLowerCase() !== dirName.toLowerCase()) { + issues.push({ + type: 'warning', + path: relativePath, + message: `Skill name "${name}" doesn't match directory "${dirName}"`, + }); + } + } + + // Check description (handles both single-line and multi-line YAML with | or >) + const descLineMatch = frontmatter.match(/^description:\s*(.*)$/m); + if (!descLineMatch) { + issues.push({ + type: 'warning', + path: relativePath, + message: 'Missing "description" in frontmatter (needed for triggers)', + }); + } else { + const indicator = descLineMatch[1].trim(); // |, >, |-, >- or empty + let description: string; + + if (indicator === '|' || indicator === '>' || indicator === '|-' || indicator === '>-') { + // Multiline YAML - extract content until next field + const descStart = frontmatter.indexOf(descLineMatch[0]) + descLineMatch[0].length; + const restOfFrontmatter = frontmatter.slice(descStart); + + // Find where next field starts + const nextFieldMatch = restOfFrontmatter.match(/\n([0-9A-Za-z_-]+):/); + const rawDesc = nextFieldMatch + ? restOfFrontmatter.slice(0, nextFieldMatch.index) + : restOfFrontmatter; + + if (indicator === '>' || indicator === '>-') { + // Folded style: newlines become spaces + description = rawDesc.split('\n').map(line => line.trimStart()).join(' ').replace(/\s+/g, ' ').trim(); + } else { + // Literal style: preserve content but remove common indentation + const lines = rawDesc.split('\n').filter(l => l.trim().length > 0); + if (lines.length > 0) { + const minIndent = lines.reduce((min, line) => { + const match = line.match(/^(\s*)/); + const indent = match ? match[1].length : 0; + return Math.min(min, indent); + }, Infinity); + description = lines.map(line => line.slice(minIndent)).join('\n').trim(); + } else { + description = ''; + } + } + } else { + // Single-line description + description = indicator; + } + + if (!description.includes('USE WHEN')) { + issues.push({ + type: 'warning', + path: relativePath, + message: 'Description should contain "USE WHEN" for trigger detection', + }); + } + } + + // Check body content (excluding frontmatter) + const bodyContent = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim(); + if (bodyContent.length < 50) { + issues.push({ + type: 'warning', + path: relativePath, + message: 'SKILL.md body is very short (< 50 chars)', + }); + } + + } catch (error) { + issues.push({ + type: 'error', + path: relativePath, + message: `Failed to read SKILL.md: ${error}`, + }); + } +} + +async function main() { + console.log('🔍 Validating skill structure...\n'); + + const result = await validateSkillStructure(); + + // Print issues + if (result.issues.length > 0) { + console.log('📋 Issues Found:\n'); + + const errors = result.issues.filter(i => i.type === 'error'); + const warnings = result.issues.filter(i => i.type === 'warning'); + + if (errors.length > 0) { + console.log('❌ Errors:'); + for (const issue of errors) { + console.log(` ${issue.path}`); + console.log(` → ${issue.message}\n`); + } + } + + if (warnings.length > 0) { + console.log('⚠️ Warnings:'); + for (const issue of warnings) { + console.log(` ${issue.path}`); + console.log(` → ${issue.message}\n`); + } + } + } else { + console.log('✅ No issues found!\n'); + } + + // Print stats + console.log('📊 Statistics:'); + console.log(` Total Skills: ${result.stats.totalSkills}`); + console.log(` 📁 Categories: ${result.stats.categories}`); + console.log(` 📄 Flat: ${result.stats.flatSkills}`); + console.log(` 📁 Hierarchical: ${result.stats.hierarchicalSkills}`); + console.log(`\n ❌ Errors: ${result.stats.errors}`); + console.log(` ⚠️ Warnings: ${result.stats.warnings}`); + + // Exit code + if (!result.valid) { + console.log('\n❌ Validation failed. Fix errors above before committing.'); + process.exit(1); + } else { + console.log('\n✅ Validation passed!'); + if (result.stats.warnings > 0) { + console.log(' (Warnings are suggestions, not blockers)'); + } + process.exit(0); + } +} + +main(); diff --git a/.opencode/PAISECURITYSYSTEM/HOOKS.md b/.opencode/PAISECURITYSYSTEM/HOOKS.md new file mode 100644 index 00000000..9eefe044 --- /dev/null +++ b/.opencode/PAISECURITYSYSTEM/HOOKS.md @@ -0,0 +1,254 @@ +# SecurityValidator Hook Documentation + +**How the security validation hook works** + +--- + +## Overview + +`SecurityValidator.hook.ts` is a PreToolUse hook that validates Bash commands and file operations against security patterns before execution. It prevents catastrophic operations while allowing normal development work. + +--- + +## Trigger + +- **Event:** PreToolUse +- **Matchers:** Bash, Edit, Write, Read + +--- + +## Input + +The hook receives JSON via stdin: + +```json +{ + "tool_name": "Bash", + "tool_input": { + "command": "rm -rf /some/path" + }, + "session_id": "abc-123-uuid" +} +``` + +For file operations: +```json +{ + "tool_name": "Write", + "tool_input": { + "file_path": "/path/to/file.txt", + "content": "..." + }, + "session_id": "abc-123-uuid" +} +``` + +--- + +## Output + +The hook communicates decisions via exit codes and stdout: + +| Exit Code | Stdout | Result | +|-----------|--------|--------| +| 0 | `{"continue": true}` | Allow operation | +| 0 | `{"decision": "ask", "message": "..."}` | Prompt user for confirmation | +| 2 | (any) | Hard block - operation prevented | + +--- + +## Pattern Loading + +The hook loads patterns in this order: + +1. **USER patterns** (primary): `USER/PAISECURITYSYSTEM/patterns.yaml` +2. **SYSTEM patterns** (fallback): `PAISECURITYSYSTEM/patterns.example.yaml` +3. **Fail-open**: If neither exists, allow all operations + +This cascading approach ensures: +- Users can customize their own security rules +- New installations work with sensible defaults +- Missing configuration doesn't block work + +--- + +## Pattern Matching + +### Bash Commands + +```yaml +bash: + blocked: # Hard block (exit 2) + - pattern: "rm -rf /" + reason: "Filesystem destruction" + + confirm: # User prompt (exit 0 + JSON) + - pattern: "git push --force" + reason: "Force push can lose commits" + + alert: # Log only + - pattern: "curl.*\\|.*sh" + reason: "Piping curl to shell" +``` + +Patterns are evaluated as regular expressions (case-insensitive). + +### Path Protection + +```yaml +paths: + zeroAccess: # Complete denial + - "~/.ssh/id_*" + + readOnly: # Can read, not write + - "/etc/**" + + confirmWrite: # Writing needs confirmation + - "**/.env" + + noDelete: # Cannot delete + - ".git/**" +``` + +Path patterns use glob syntax: +- `*` matches any characters except `/` +- `**` matches any characters including `/` +- `~` expands to home directory + +--- + +## Execution Flow + +``` +1. Parse stdin JSON +2. Load patterns (USER → SYSTEM → empty) +3. Determine tool type (Bash vs file operation) +4. For Bash: Check command against bash patterns +5. For files: Check path against path patterns +6. Log security event (all decisions) +7. Return decision (exit code + JSON) +``` + +--- + +## Security Event Logging + +All decisions are logged as individual files: `MEMORY/SECURITY/YYYY/MM/security-{summary}-{timestamp}.jsonl` + +Each event gets a descriptive filename (e.g., `security-block-filesystem-destruction-20260114-143052.jsonl`). + +```json +{ + "timestamp": "2026-01-14T12:00:00.000Z", + "session_id": "abc-123", + "event_type": "block", + "tool": "Bash", + "category": "bash_command", + "target": "rm -rf /", + "pattern_matched": "rm -rf /", + "reason": "Filesystem destruction", + "action_taken": "blocked" +} +``` + +--- + +## Configuration + +Enable the hook in `settings.json`: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [{ + "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" + }] + }, + { + "matcher": "Edit", + "hooks": [{ + "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" + }] + }, + { + "matcher": "Write", + "hooks": [{ + "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" + }] + }, + { + "matcher": "Read", + "hooks": [{ + "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" + }] + } + ] + } +} +``` + +--- + +## Error Handling + +The hook is designed to fail-open for usability: + +| Error | Behavior | +|-------|----------| +| Missing patterns.yaml | Allow all operations | +| YAML parse error | Log warning, allow operation | +| Invalid pattern regex | Try literal match | +| Logging failure | Silent (doesn't block) | + +--- + +## Performance + +- **Blocking:** Yes (must complete before tool executes) +- **Typical execution:** <10ms +- **Design:** Fast path for safe operations, pattern matching only when needed +- **Caching:** Patterns are cached after first load + +--- + +## Testing + +Test the hook directly: + +```bash +# Test a blocked command +echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"session_id":"test"}' | \ + bun ~/.opencode/hooks/SecurityValidator.hook.ts +# Should exit 2 (blocked) + +# Test an allowed command +echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"test"}' | \ + bun ~/.opencode/hooks/SecurityValidator.hook.ts +# Should output {"continue": true} and exit 0 + +# Test a confirm command +echo '{"tool_name":"Bash","tool_input":{"command":"git push --force"},"session_id":"test"}' | \ + bun ~/.opencode/hooks/SecurityValidator.hook.ts +# Should output {"decision": "ask", ...} and exit 0 +``` + +--- + +## Customization + +To add custom patterns: + +1. Create `USER/PAISECURITYSYSTEM/patterns.yaml` (copy from `PAISECURITYSYSTEM/patterns.example.yaml`) +2. Add patterns to appropriate sections +3. Patterns are loaded on next hook invocation (restart session) + +Example custom pattern: +```yaml +bash: + blocked: + - pattern: "npm publish" + reason: "Accidental package publish" +``` diff --git a/.opencode/skills/skill-index.json b/.opencode/skills/skill-index.json index 7f532052..b2fb7c4f 100644 --- a/.opencode/skills/skill-index.json +++ b/.opencode/skills/skill-index.json @@ -1,11 +1,11 @@ { - "generated": "2026-03-12T08:28:30.465Z", - "totalSkills": 54, + "generated": "2026-03-19T23:43:15.603Z", + "totalSkills": 52, "categories": 7, - "flatSkills": 19, - "hierarchicalSkills": 35, + "flatSkills": 11, + "hierarchicalSkills": 41, "alwaysLoadedCount": 2, - "deferredCount": 52, + "deferredCount": 50, "skills": { "agents": { "name": "Agents", @@ -129,8 +129,8 @@ }, "audioeditor": { "name": "AudioEditor", - "path": "AudioEditor/SKILL.md", - "category": null, + "path": "Utilities/AudioEditor/SKILL.md", + "category": "Utilities", "fullDescription": "AI-powered audio/video editing — transcription, intelligent cut detection, automated editing with crossfades, and optional cloud polish. USE WHEN clean audio, edit audio, remove filler words, clean podcast, remove ums, fix audio, cut dead air, polish audio, clean recording, transcribe and edit.", "triggers": [ "clean", @@ -153,7 +153,7 @@ "Clean" ], "tier": "deferred", - "isHierarchical": false + "isHierarchical": true }, "becreative": { "name": "BeCreative", @@ -236,8 +236,8 @@ }, "codereview": { "name": "CodeReview", - "path": "CodeReview/SKILL.md", - "category": null, + "path": "Utilities/CodeReview/SKILL.md", + "category": "Utilities", "fullDescription": "AI-powered code review via roborev. USE WHEN review code, check code quality, roborev, audit changes, review before commit, review before PR, code quality check, lint review, architecture review.", "triggers": [ "review", @@ -254,7 +254,7 @@ ], "workflows": [], "tier": "deferred", - "isHierarchical": false + "isHierarchical": true }, "contentanalysis": { "name": "ContentAnalysis", @@ -557,8 +557,8 @@ }, "opencodesystem": { "name": "OpenCodeSystem", - "path": "OpenCodeSystem/SKILL.md", - "category": null, + "path": "Utilities/OpenCodeSystem/SKILL.md", + "category": "Utilities", "fullDescription": "PAI-OpenCode system self-awareness. USE WHEN asking about tools, config, model routing, plugin handlers, MCP servers, troubleshooting, or operating environment.", "triggers": [ "asking", @@ -578,7 +578,7 @@ "PAI" ], "tier": "deferred", - "isHierarchical": false + "isHierarchical": true }, "osint": { "name": "OSINT", @@ -606,16 +606,6 @@ "tier": "deferred", "isHierarchical": true }, - "pai": { - "name": "PAI", - "path": "PAI/SKILL.md", - "category": null, - "fullDescription": "Personal AI Infrastructure core. The authoritative reference for how PAI works.", - "triggers": [], - "workflows": [], - "tier": "deferred", - "isHierarchical": false - }, "paiupgrade": { "name": "PAIUpgrade", "path": "Utilities/PAIUpgrade/SKILL.md", @@ -868,8 +858,8 @@ }, "sales": { "name": "Sales", - "path": "Sales/SKILL.md", - "category": null, + "path": "Utilities/Sales/SKILL.md", + "category": "Utilities", "fullDescription": "Sales workflows. USE WHEN sales, proposal, pricing. SkillSearch('sales') for docs.", "triggers": [ "sales", @@ -882,7 +872,7 @@ "Create-visual" ], "tier": "deferred", - "isHierarchical": false + "isHierarchical": true }, "science": { "name": "Science", @@ -1003,8 +993,8 @@ }, "system": { "name": "System", - "path": "System/SKILL.md", - "category": null, + "path": "Utilities/System/SKILL.md", + "category": "Utilities", "fullDescription": "System maintenance - integrity check, document session, secret scanning. USE WHEN integrity, audit, document session, secrets, security scan.", "triggers": [ "integrity", @@ -1027,7 +1017,7 @@ "WorkContextRecall" ], "tier": "deferred", - "isHierarchical": false + "isHierarchical": true }, "telos": { "name": "Telos", @@ -1141,24 +1131,6 @@ "tier": "deferred", "isHierarchical": false }, - "voiceserver": { - "name": "VoiceServer", - "path": "VoiceServer/SKILL.md", - "category": null, - "fullDescription": "Voice server management. USE WHEN voice server, TTS server, voice notification, prosody.", - "triggers": [ - "voice", - "server", - "tts", - "notification", - "prosody" - ], - "workflows": [ - "Status" - ], - "tier": "deferred", - "isHierarchical": false - }, "webassessment": { "name": "WebAssessment", "path": "Security/WebAssessment/SKILL.md", @@ -1218,8 +1190,8 @@ }, "writestory": { "name": "WriteStory", - "path": "WriteStory/SKILL.md", - "category": null, + "path": "Utilities/WriteStory/SKILL.md", + "category": "Utilities", "fullDescription": "Layered fiction writing system using Will Storr's storytelling science and rhetorical figures. USE WHEN write story, fiction, novel, short story, book, chapter, story bible, character arc, plot outline, creative writing, worldbuilding, narrative, mystery writing, dialogue, prose, series planning.", "triggers": [ "write", @@ -1252,7 +1224,7 @@ "Revise" ], "tier": "deferred", - "isHierarchical": false + "isHierarchical": true }, "xlsx": { "name": "Xlsx", @@ -1303,8 +1275,10 @@ ], "Utilities": [ "Aphorisms", + "AudioEditor", "Browser", "Cloudflare", + "CodeReview", "CreateCLI", "CreateSkill", "Delegation", @@ -1312,11 +1286,15 @@ "Docx", "Evals", "Fabric", + "OpenCodeSystem", "PAIUpgrade", "Parser", "Pdf", "Pptx", "Prompting", + "Sales", + "System", + "WriteStory", "Xlsx" ] } From 353186557cb464d655fcdc89b5bced01f04ea5b6 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:06:34 -0400 Subject: [PATCH 5/8] chore: remove INSTALLER-REFACTOR-PLAN.md (internal planning doc) --- .prd/PRD-20260309-coderabbit-pr47-fixes.md | 316 ----- .prd/PRD-20260309-installer-refactor.md | 102 -- docs/architecture/INSTALLER-REFACTOR-PLAN.md | 1122 ------------------ 3 files changed, 1540 deletions(-) delete mode 100644 .prd/PRD-20260309-coderabbit-pr47-fixes.md delete mode 100644 .prd/PRD-20260309-installer-refactor.md delete mode 100644 docs/architecture/INSTALLER-REFACTOR-PLAN.md diff --git a/.prd/PRD-20260309-coderabbit-pr47-fixes.md b/.prd/PRD-20260309-coderabbit-pr47-fixes.md deleted file mode 100644 index 1e1524d4..00000000 --- a/.prd/PRD-20260309-coderabbit-pr47-fixes.md +++ /dev/null @@ -1,316 +0,0 @@ ---- -prd: true -id: PRD-20260309-coderabbit-pr47-fixes -status: COMPLETE -mode: interactive -effort_level: Comprehensive -created: 2026-03-09 -updated: 2026-03-09 -iteration: 2 -maxIterations: 128 -loopStatus: completed -last_phase: VERIFY -failing_criteria: [] -verification_summary: "43/43" -parent: null -children: [] ---- - -# CodeRabbit PR #47 — Bug Fixes - -> Address ALL 43 verified CodeRabbit findings from PR #47 in pai-opencode, covering -> security vulnerabilities, data-loss bugs, syntax errors, XSS issues, path traversal, -> missing imports, and incorrect paths. - ---- - -## STATUS - -| What | State | -|------|-------| -| Progress | 43/43 criteria passing | -| Phase | COMPLETE | -| Next action | Commit changes to PR #47 | -| Blocked by | nothing | - ---- - -## CONTEXT - -### Problem Space - -CodeRabbit reviewed PR #47 and posted 35 comments (8 critical, 23 major, 12 minor). -Each finding was verified against the actual current code. 10 are confirmed real bugs -that need fixing. The remaining findings are either already-addressed, out of scope, -or lower priority than these 10. - -### Verified Findings (all 10 confirmed against current code) - -| # | File | Lines | Severity | Issue | -|---|------|-------|----------|-------| -| 1 | `.opencode/plugins/lib/db-utils.ts` | 62–112 | 🔴 Critical | `archiveSessions` copies to archive but never deletes from source DB — data not actually moved | -| 2 | `.opencode/plugins/lib/db-utils.ts` | 37–57, 62–112 | 🟠 Major | `getSessionsOlderThan` and `archiveSessions` open DB handles via `getDb()` but never close them | -| 3 | `PAI-Install/cli/index.ts` | 206–225 | 🔴 Critical | When `allCritical` is false, code still calls `generateSummary`, `printSummary`, `clearState()`, and `process.exit(0)` — resume state is destroyed on failure | -| 4 | `PAI-Install/electron/package.json` | 9–11 | 🟠 Major | `electron: "^34.0.0"` is a known-vulnerable version; minimum safe is 35.7.5 | -| 5 | `PAI-Install/engine/state.ts` | 122–134 | 🔴 Critical | `skipStep` has a duplicate `saveState(state)` call at line 133 and a stray `}` at line 134 — syntax error that prevents compilation | -| 6 | `PAI-Install/engine/state.ts` | 129 | 🟡 Minor | `// eslint-disable-next-line` comment — project uses Biome exclusively, ESLint comments are anti-pattern | -| 7 | `PAI-Install/web/server.ts` | 73–79 | 🔴 Critical | Path traversal check uses `fullPath.startsWith(PUBLIC_DIR)` which is bypassable; must use `resolve` + `relative` | -| 8 | `PAI-Install/web/server.ts` | 64–69 | 🟠 Major | WebSocket upgrade has no Origin validation — any local page can connect | -| 9 | `Tools/db-archive.ts` | 17 | 🔴 Critical | `mkdirSync` is called at line 110 but not imported from `node:fs` — runtime crash on archive | -| 10 | `Tools/db-archive.ts` | 40–50 | 🟠 Major | `parseArgs()` only handles `--restore=value` form; `--restore archive.db` (space-separated as documented) silently falls through | - -### Key Files - -| File | Role | -|------|------| -| `.opencode/plugins/lib/db-utils.ts` | DB utilities: session queries, archiving, health checks | -| `PAI-Install/cli/index.ts` | CLI install wizard — orchestrates 8 install steps | -| `PAI-Install/engine/state.ts` | Install state persistence (save/load/clear/skip/complete) | -| `PAI-Install/electron/package.json` | Electron wrapper package manifest | -| `PAI-Install/web/server.ts` | Bun HTTP + WebSocket server for web installer UI | -| `Tools/db-archive.ts` | CLI tool for archiving and vacuuming the conversations DB | - -### Constraints - -- Use Bun (`bun:sqlite`) not Node sqlite -- Biome for linting — no ESLint comments -- All TypeScript strict mode -- `node:` prefix on built-in imports -- Do NOT refactor beyond the minimal fix for each issue - ---- - -## PLAN - -Fix files in this order (dependency-safe, smallest blast radius first): - -1. **`Tools/db-archive.ts`** — Add `mkdirSync` to import + fix `--restore` parser (ISC-C9, ISC-C10) -2. **`PAI-Install/engine/state.ts`** — Remove duplicate `saveState` + stray brace + eslint comment (ISC-C5, ISC-C6) -3. **`PAI-Install/cli/index.ts`** — Fix allCritical false branch to exit early without clearState (ISC-C3) -4. **`PAI-Install/electron/package.json`** — Bump electron to ^35.7.5 (ISC-C4) -5. **`PAI-Install/web/server.ts`** — Fix path traversal + add WS Origin check (ISC-C7, ISC-C8) -6. **`.opencode/plugins/lib/db-utils.ts`** — Fix DB handle leaks + add DELETE after archive insert (ISC-C1, ISC-C2) - -Each fix is surgical — minimum lines changed to satisfy the ISC criterion. - -### Fix Details - -#### Fix 1 — Tools/db-archive.ts (ISC-C9 + ISC-C10) - -```typescript -// Line 17: add mkdirSync to import -import { existsSync, statSync, mkdirSync } from "node:fs"; - -// parseArgs(): handle space-separated --restore -function parseArgs(): Options { - const args = process.argv.slice(2); - const daysArg = args.find((a) => /^\d+$/.test(a)); - const restoreIdx = args.findIndex((a) => a === "--restore"); - - return { - days: daysArg ? parseInt(daysArg, 10) : 90, - dryRun: args.includes("--dry-run"), - vacuum: args.includes("--vacuum"), - restore: - args.find((a) => a.startsWith("--restore="))?.split("=")[1] || - (restoreIdx !== -1 ? args[restoreIdx + 1] || null : null), - }; -} -``` - -#### Fix 2 — PAI-Install/engine/state.ts (ISC-C5 + ISC-C6) - -Remove lines 133–134 (duplicate `saveState(state)` and stray `}`). -Remove the `// eslint-disable-next-line @typescript-eslint/no-unused-expressions` comment on line 129. -Replace `reason;` no-op with a proper `void reason;` or simply remove the line if `reason` is unused. - -```typescript -export function skipStep(state: InstallState, step: StepId, nextStep?: StepId, reason?: string): void { - if (!state.skippedSteps.includes(step)) { - state.skippedSteps.push(step); - } - if (nextStep) { - state.currentStep = nextStep; - } - // reason parameter reserved for future logging - saveState(state); -} -``` - -#### Fix 3 — PAI-Install/cli/index.ts (ISC-C3) - -```typescript -const allCritical = checks.filter((c) => c.critical).every((c) => c.passed); -if (!allCritical) { - printError("\nSome critical checks failed. Please review and fix the issues above."); - printInfo("Your progress has been saved. Run the installer again to resume."); - process.exit(1); -} -completeStep(state, "validation"); - -// ── Summary ── -const summary = generateSummary(state); -printSummary(summary); -clearState(); -// ... success messages and process.exit(0) -``` - -#### Fix 4 — PAI-Install/electron/package.json (ISC-C4) - -```json -"electron": "^35.7.5" -``` - -#### Fix 5 — PAI-Install/web/server.ts (ISC-C7 + ISC-C8) - -Path traversal — replace `startsWith` with `resolve`+`relative`: -```typescript -import { resolve, relative, join, extname } from "path"; - -// In fetch handler: -const requestedPath = url.pathname === "/" ? "index.html" : url.pathname.slice(1); -const fullPath = resolve(PUBLIC_DIR, requestedPath); -const rel = relative(PUBLIC_DIR, fullPath); -if (rel.startsWith("..") || rel === "..") { - return new Response("Forbidden", { status: 403 }); -} -``` - -WebSocket Origin check: -```typescript -if (url.pathname === "/ws") { - const origin = req.headers.get("origin"); - const allowedOrigins = [ - `http://127.0.0.1:${PORT}`, - `http://localhost:${PORT}`, - ]; - if (!origin || !allowedOrigins.includes(origin)) { - return new Response("Forbidden", { status: 403 }); - } - const upgraded = server.upgrade(req); - // ... -} -``` - -#### Fix 6 — .opencode/plugins/lib/db-utils.ts (ISC-C1 + ISC-C2) - -`getSessionsOlderThan`: close db handle after query. -`archiveSessions`: open db as writable (not readonly), close db handle at end, -and delete source records after successful insert: - -```typescript -// archiveSessions: open writable for DELETE -const { Database } = require("bun:sqlite"); -const db = new Database(DB_PATH, { readonly: false }); - -// After successful archiveDb.run INSERT: -db.run("DELETE FROM messages WHERE conversation_id = ?", [session.id]); -db.run("DELETE FROM conversations WHERE id = ?", [session.id]); -archived++; - -// At end: -db.close(); -archiveDb.close(); -``` - ---- - -## IDEAL STATE CRITERIA (All 43 Verified and Fixed) - -### Critical Security & Data Integrity (10) - -- [x] **ISC-C1:** archiveSessions deletes source records after archive insert | Verify: Grep "DELETE FROM" — **2 DELETE statements added** -- [x] **ISC-C2:** getSessionsOlderThan and archiveSessions close DB handles | Verify: Read try/finally blocks — **all handles closed** -- [x] **ISC-C3:** CLI validation failure exits non-zero without clearing state | Verify: Read process.exit(1) before summary — **fixed** -- [x] **ISC-C7:** Path traversal uses resolve+relative not startsWith | Verify: Read resolve/relative check — **fixed** -- [x] **ISC-C8:** WebSocket upgrade validates Origin header | Verify: Read origin whitelist check — **implemented** -- [x] **ISC-C20:** renderSummary uses createElement not innerHTML | Verify: Read DOM API usage — **XSS eliminated** -- [x] **ISC-C22:** renderSteps uses createElement not innerHTML | Verify: Read DOM API usage — **XSS eliminated** -- [x] **ISC-C17:** db-archive command respects args.dryRun/vacuum/days | Verify: Read param handling — **now uses args** -- [x] **ISC-C14:** migration-v2-to-v3.ts avoids Foo/Foo double paths | Verify: Read hierarchical check — **basename check added** -- [x] **ISC-C16:** migration-v2-to-v3.ts v3-dual-config triggers v3 path | Verify: Read version check — **added v3-dual-config check** - -### Major Functionality (15) - -- [x] **ISC-C4:** Electron dependency ≥35.7.5 | Verify: Read package.json — **bumped from ^34.0.0** -- [x] **ISC-C5:** skipStep has no duplicate saveState or stray brace | Verify: Static build — **syntax fixed** -- [x] **ISC-C6:** No eslint-disable comments | Verify: Grep eslint — **removed, using void** -- [x] **ISC-C9:** db-archive.ts imports mkdirSync | Verify: Grep import — **added to import** -- [x] **ISC-C10:** parseArgs handles --restore space-separated | Verify: Read indexOf logic — **space form now works** -- [x] **ISC-C11:** USMetrics/SKILL.md single frontmatter | Verify: Read frontmatter — **consolidated to one** -- [x] **ISC-C12:** USMetrics/SKILL.md follows PAI v3.0 format | Verify: Read USE WHEN triggers — **format updated** -- [x] **ISC-C13:** generate-welcome.ts uses ~/.opencode | Verify: Read path — **changed from ~/.claude** -- [x] **ISC-C15:** electron/main.js waitForServer verifies Bun | Verify: Read HTTP health check — **verifies it's PAI** -- [x] **ISC-C18:** cli/index.ts saves currentStep before completeStep | Verify: Read state mutations — **order fixed** -- [x] **ISC-C19:** config-gen.ts repoUrl to Steffen025/pai-opencode | Verify: Read URL — **fixed from danielmiessler/PAI** -- [x] **ISC-C21:** web/routes.ts pendingRequests cleanup | Verify: Read timeout mechanism — **5-min timeout added** -- [x] **ISC-C23:** actions.ts fallback writes complete settings | Verify: Read permissions/plansDirectory — **added to config-gen** -- [x] **ISC-C24:** actions.ts kills voice server by PID check | Verify: Read Bun process check — **verifies Bun before kill** -- [x] **ISC-C25:** actions.ts chmod only specific scripts | Verify: Read find/chmod commands — **scoped to scripts** - -### Minor Quality (8) - -- [x] **ISC-C26:** session-cleanup.ts indentation consistent | Verify: Read if block — **fixed** -- [x] **ISC-C27:** install.sh command check matches output | Verify: Read command and message — **claude→opencode** -- [x] **ISC-C28:** CHANGELOG.md Tools/ capitalization | Verify: Read paths — **fixed 2 occurrences** -- [x] **ISC-C29:** README.md skill count 52 | Verify: Read 44 more — **fixed from 31** -- [x] **ISC-C30:** steps.ts ~/.opencode not ~/.claude | Verify: Read description — **fixed** -- [x] **ISC-C31:** main.ts validates --mode values | Verify: Read validation — **validModes added** -- [x] **ISC-C32:** types.ts comment ~/.opencode | Verify: Read comment — **fixed** -- [x] **ISC-C33:** app.js JSON.parse error handling | Verify: Read try/catch — **added** - -### Anti-Criteria (10) - -- [x] **ISC-A1:** No source sessions remain after archive | Verify: Read DELETE statements — **verified** -- [x] **ISC-A2:** No DB handle leaks | Verify: Read db.close() calls — **4 close calls** -- [x] **ISC-A3:** No successful path on critical failure | Verify: Read early exit — **verified** -- [x] **ISC-A4:** No syntax errors in state.ts | Verify: Build — **passes** -- [x] **ISC-A5:** No path traversal vulnerability | Verify: Read ".." guard — **verified** -- [x] **ISC-A6:** No XSS in renderSummary | Verify: Read createElement usage — **verified** -- [x] **ISC-A7:** No XSS in renderSteps | Verify: Read createElement usage — **verified** -- [x] **ISC-A8:** No blind port killing | Verify: Read Bun check — **verified** -- [x] **ISC-A9:** No over-permissive chmod | Verify: Read scoped chmod — **verified** -- [x] **ISC-A10:** No pendingRequest memory leak | Verify: Read timeout cleanup — **verified** - ---- - -## DECISIONS - -| Date | Decision | Rationale | -|------|----------|-----------| -| 2026-03-09 | Fix ALL 43 verified findings | User explicitly requested all CodeRabbit issues be fixed, not just 10 | -| 2026-03-09 | Keep `archiveSessions` opening db as writable | DELETE requires write access; `getDb()` uses readonly and can't be reused here | -| 2026-03-09 | Origin whitelist: 127.0.0.1 and localhost only | Server already binds to 127.0.0.1; these are the only valid origins for the local installer UI | -| 2026-03-09 | Electron bump to ^35.7.5 not ^40.x | CR specified 35.7.5 as minimum safe; jumping to latest major may require additional testing | -| 2026-03-09 | XSS fix: Use createElement/textContent instead of innerHTML | DOM API approach is safer than trying to sanitize HTML | -| 2026-03-09 | pendingRequest timeout: 5 minutes | Balance between user time to respond and memory leak prevention | -| 2026-03-09 | Voice server kill: Check process name contains "bun" | Prevents killing unrelated processes on port 8888 | - ---- - -## LOG - -### Iteration 1 — 2026-03-09 (COMPLETE) -- Phase reached: VERIFY → COMPLETE -- Criteria progress: 15/15 (10 ISC-C + 5 ISC-A) -- Work done: - 1. Tools/db-archive.ts — Added `mkdirSync` import (ISC-C9), fixed `--restore` parser to handle space-separated form (ISC-C10) - 2. PAI-Install/engine/state.ts — Removed duplicate `saveState()` + stray brace (ISC-C5), removed eslint-disable comment (ISC-C6) - 3. PAI-Install/cli/index.ts — Fixed validation failure path to exit with code 1 without calling clearState() (ISC-C3 + ISC-A3) - 4. PAI-Install/electron/package.json — Bumped electron to ^35.7.5 (ISC-C4) - 5. PAI-Install/web/server.ts — Replaced startsWith with resolve+relative for path traversal (ISC-C7 + ISC-A5), added Origin header validation for WebSocket (ISC-C8) - 6. .opencode/plugins/lib/db-utils.ts — Added DELETE statements after successful archive (ISC-C1 + ISC-A1), added try/finally blocks to close all DB handles (ISC-C2 + ISC-A2) -- Verification: All files compile with `bun build`; Biome check shows only pre-existing style issues, no new errors introduced -- Failing: none -- Context for next session: ALL 43 fixes complete. Ready to commit to PR #47. - -### Iteration 2 — 2026-03-09 (Additional 33 fixes) -- Phase reached: BUILD → VERIFY -- Criteria progress: 43/43 (33 ISC-C + 10 ISC-A) -- Mass fix execution: 21 additional files modified including XSS fixes, path corrections, validation improvements -- All CodeRabbit findings addressed - -### Iteration 0 — 2026-03-09 -- Phase reached: PLAN -- Criteria progress: 0/43 -- Work done: Verified all findings against actual code, created PRD diff --git a/.prd/PRD-20260309-installer-refactor.md b/.prd/PRD-20260309-installer-refactor.md deleted file mode 100644 index e55e7f33..00000000 --- a/.prd/PRD-20260309-installer-refactor.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -prd: true -id: PRD-20260309-installer-refactor -status: IN_PROGRESS -mode: interactive -effort_level: Extended -created: 2026-03-09 -updated: 2026-03-09 -iteration: 0 -maxIterations: 1 -loopStatus: null -last_phase: PLAN -failing_criteria: [] -verification_summary: "0/17" -parent: null -children: [] ---- - -# PAI-OpenCode Installer Refactor Implementation - -> Implement installer refactoring per docs/architecture/INSTALLER-REFACTOR-PLAN.md -> Branch: feature/wp-e-installer-refactor -> Target: PR #48 - -## STATUS - -| What | State | -|------|-------| -| Progress | 0/17 criteria passing | -| Phase | PLAN → BUILD | -| Next action | Create feature branch, implement engine files | -| Blocked by | None | - -## CONTEXT - -### Problem Space -Current installer has 4 entry points causing user confusion. Need ONE unified Electron GUI with auto-detection for fresh/migrate/update modes. Must integrate wrapper system from reference implementation. - -### Key Files -- `PAI-Install/engine/build-opencode.ts` — Build OpenCode binary (NEW) -- `PAI-Install/engine/migrate.ts` — v2→v3 migration (NEW) -- `PAI-Install/engine/update.ts` — v3→v3.x updates (NEW) -- `/usr/local/bin/{AI_NAME}-wrapper` — Wrapper script (NEW) -- `~/.opencode/tools/opencode` — Custom binary symlink (NEW) -- `PAI-Install/install.sh` — Simplified to 15-20 lines (EDIT) - -### Constraints -- NO automatic migration without consent -- NO overwriting existing backups -- NO using Homebrew opencode as default -- NO breaking existing .zshrc configurations - -## PLAN - -1. Create feature branch `feature/wp-e-installer-refactor` -2. Implement engine/build-opencode.ts (port from PAIOpenCodeWizard.ts) -3. Implement engine/migrate.ts (port from Tools/migration-v2-to-v3.ts) -4. Implement engine/update.ts (new) -5. Implement step files (steps-fresh, steps-migrate, steps-update) -6. Create wrapper script at /usr/local/bin/{AI_NAME}-wrapper -7. Add .zshrc alias integration -8. Update Electron UI for flow routing -9. Simplify install.sh to 15-20 lines -10. Create cli/quick-install.ts for headless mode -11. Delete 6 deprecated files -12. Test all scenarios - -## IDEAL STATE CRITERIA - -- [ ] ISC-C1: install.sh is exactly 15-20 lines of bash -- [ ] ISC-C2: engine/build-opencode.ts builds custom OpenCode binary with progress callbacks -- [ ] ISC-C3: engine/migrate.ts ports v2→v3 migration with backup creation -- [ ] ISC-C4: engine/update.ts handles v3→v3.x updates preserving settings -- [ ] ISC-C5: Wrapper script installed at /usr/local/bin/{AI_NAME}-wrapper -- [ ] ISC-C6: Custom binary symlinked at ~/.opencode/tools/opencode -- [ ] ISC-C7: .zshrc alias created and persists after restart -- [ ] ISC-C8: Electron UI auto-detects fresh/migrate/update modes -- [ ] ISC-C9: OpenCode-Zen is default provider (FREE tier emphasized) -- [ ] ISC-C10: Build step shows live progress (10-70%) with skip option -- [ ] ISC-C11: Migration requires explicit user consent with backup -- [ ] ISC-C12: Headless CLI mode works with all arguments -- [ ] ISC-C13: 6 deprecated files deleted - -### Anti-Criteria -- [ ] ISC-A1: NO automatic migration without user confirmation -- [ ] ISC-A2: NO overwriting existing backups -- [ ] ISC-A3: NO using Homebrew opencode as default -- [ ] ISC-A4: NO breaking existing .zshrc configurations - -## DECISIONS - -- 2026-03-09: Use OpenCode-Zen as default provider (FREE tier) per Jeremy clarification -- 2026-03-09: Wrapper script pattern based on existing ~/.opencode/tools/opencode-wrapper -- 2026-03-09: Build from source (don't bundle binary) due to GitHub size limits -- 2026-03-09: Migration requires explicit consent with backup creation - -## LOG - -### Iteration 0 — 2026-03-09 -- Phase reached: PLAN -- Created 17 ISC criteria -- Ready to create feature branch and implement diff --git a/docs/architecture/INSTALLER-REFACTOR-PLAN.md b/docs/architecture/INSTALLER-REFACTOR-PLAN.md deleted file mode 100644 index 1b371c17..00000000 --- a/docs/architecture/INSTALLER-REFACTOR-PLAN.md +++ /dev/null @@ -1,1122 +0,0 @@ ---- -title: "PAI-OpenCode Installer Refactor Plan (Updated)" -status: "Ready for Implementation — Post PR #47" -date: "2026-03-09" -tags: [architecture, installer, roadmap] ---- - -# PAI-OpenCode Installer Refactor Plan (Updated) - -> [!info] -> **Status:** Ready for Implementation — Post PR `#47` -> **Goal:** One Electron GUI entry point for both new and existing users -> **Author:** Jeremy (Updated after WP-D completion) -> **Target:** New PR `#48` (after PR `#47` merged) - ---- - -## 1. Current State (Post PR #47) - -### ✅ What Was Fixed in PR #47 - -PR #47 successfully merged PAI-Install v4.0.3 with all CodeRabbit fixes: - -- ✅ Git URLs corrected to `Steffen025/pai-opencode` -- ✅ Atomic file writes in `engine/state.ts` -- ✅ `opencode.json` validation added -- ✅ Fish shell alias detection working -- ✅ Safe headless detection with `${DISPLAY-}` -- ✅ 4 fixes in `generate-welcome.ts` -- ✅ Target-specific client sockets + inputType masking -- ✅ Voice IDs have secret allowlist comments -- ✅ Retry limit (50 attempts) in `checkAndSend` -- ✅ Brew detection cached (no duplicate exec) -- ✅ `db-archive.ts` success/failure logic fixed -- ✅ `migration-v2-to-v3.ts` syntax errors resolved -- ✅ Command help text clarified (shows stats only) -- ✅ README callout syntax applied - -### ❌ What Still Needs Refactoring - -**Current installer structure (messy):** -```text -install.sh ← 163 lines (too complex) - └── PAI-Install/ - ├── cli/ ← 3 files (TUI, interactive) - ├── electron/ ← GUI wrapper (separate) - ├── engine/ ← 8 files (shared logic) - └── web/ ← Web server for Electron - -.opencode/PAIOpenCodeWizard.ts ← STILL EXISTS (4. Weg!) -Tools/migration-v2-to-v3.ts ← STILL EXISTS (separate script) -``` - -**Problems identified:** -1. **4 entry points still exist** — user confusion not resolved -2. **PAIOpenCodeWizard.ts not integrated** — build logic lives outside PAI-Install -3. **Migration is separate** — not unified with installer -4. **TUI code (cli/)** — duplicates what Electron should do -5. **install.sh too complex** — 163 lines of bash - ---- - -## 2. Clarifications from PR #47 - -### 2.1 What the Installer Actually Does - -**Clarified:** The installer has TWO distinct responsibilities: - -| Phase | What It Does | Where Logic Lives | -|-------|--------------|-------------------| -| **Bootstrap** | Check/install bun, launch Electron | `install.sh` | -| **Build OpenCode** | Clone fork, checkout model-tiers, build binary | `PAIOpenCodeWizard.ts` ❌ (external!) | -| **Install PAI** | Copy files, generate settings, setup voice | `PAI-Install/engine/` ✅ | -| **Migrate** | v2→v3 structure migration, backup | `Tools/migration-v2-to-v3.ts` ❌ (external!) | - -**Problem:** The Build and Migrate logic are OUTSIDE PAI-Install, causing the fragmentation. - -### 2.2 User Scenarios Clarified - -| User Type | Current Experience | Target Experience | -|-----------|-------------------|-------------------| -| **New User** | Reads README, confused which script to run | `bash install.sh` → Electron auto-detects "fresh" | -| **v2→v3 Migrator** | Runs `migration-v2-to-v3.ts`, then installer | `bash install.sh` → Electron auto-detects "migrate" | -| **v3 Updater** | Manual git pull, no installer | `bash install.sh` → Electron auto-detects "update" | -| **CI/Headless** | No supported path | `bash install.sh --cli --preset anthropic` | - -### 2.3 What "Building OpenCode Binary" Actually Means - -**Clarified:** This is NOT installing PAI — it's building a custom OpenCode CLI tool: - -```text -Steffen025/opencode (fork) - └── feature/model-tiers (branch with 60x cost optimization) - └── bun build → /usr/local/bin/opencode (binary) -``` - -**Why it's needed:** -- Model Tier routing (quick=MiniMax, standard=Sonnet, advanced=Opus) -- 60x cost optimization (Opus vs MiniMax cost difference) -- PAI-specific enhancements - -**Why it's confusing:** Users think they're installing PAI, but first they must build a custom OpenCode binary. - -### 2.4 Migration vs. Update Clarified - -| Operation | When | What Changes | -|-----------|------|--------------| -| **Migrate (v2→v3)** | Flat skills → Hierarchical | Skills structure, MINIMAL_BOOTSTRAP | -| **Update (v3→v3.x)** | Within v3.x versions | PAI files, skills, maybe OpenCode binary | -| **Fresh Install** | No existing ~/.opencode | Everything: OpenCode binary + PAI files | - -**Detection Logic:** -```typescript -function detectInstallMode(): "fresh" | "migrate-v2" | "update-v3" | "current" { - const opencodePath = path.join(os.homedir(), ".opencode"); - if (!existsSync(opencodePath)) return "fresh"; - - const settings = readSettings(); - if (settings?.pai?.version?.startsWith("3")) { - // Has v3, check if update needed - return isOutdated(settings.pai.version) ? "update-v3" : "current"; - } - - // Has .opencode but no v3 settings = v2 - return "migrate-v2"; -} -``` - ---- - -## 3. Updated Target Architecture - -### Simplified Structure - -```text -PAI-Install/ -├── install.sh ← Bootstrap ONLY (15 lines) -├── README.md ← Entry point docs -│ -├── electron/ ← PRIMARY ENTRY POINT -│ ├── main.js ← Electron main process -│ ├── package.json ← electron deps -│ └── preload.js ← Security context bridge -│ -├── engine/ ← SHARED LOGIC -│ ├── detect.ts ← System + install mode detection -│ ├── build-opencode.ts ← ⭐ NEW: Build OpenCode binary -│ ├── migrate.ts ← ⭐ NEW: v2→v3 migration -│ ├── update.ts ← ⭐ NEW: v3→v3.x update -│ ├── actions.ts ← Install actions -│ ├── config-gen.ts ← Settings generation -│ ├── state.ts ← State machine (already atomic ✓) -│ ├── validate.ts ← Validation (already has opencode.json ✓) -│ ├── steps-fresh.ts ← ⭐ NEW: 8-step fresh install -│ ├── steps-migrate.ts ← ⭐ NEW: 5-step migration -│ ├── steps-update.ts ← ⭐ NEW: 3-step update -│ └── types.ts ← Types (already has DEFAULT_VOICES ✓) -│ -├── web/ ← Web UI (served by bun) -│ ├── server.ts ← Bun HTTP server -│ ├── routes.ts ← API routes (already has socket targeting ✓) -│ └── public/ -│ ├── index.html -│ ├── app.js ← UI (already has retry limit ✓) -│ ├── styles.css -│ └── assets/ -│ -└── cli/ ← HEADLESS ONLY - └── quick-install.ts ← ⭐ RENAMED from index.ts, non-interactive -``` - -### Deleted Files - -| File | Status | Notes | -|------|--------|-------| -| `cli/display.ts` | ❌ DELETE | TUI replaced by Electron | -| `cli/index.ts` | ❌ DELETE | Interactive flow replaced | -| `cli/prompts.ts` | ❌ DELETE | Terminal prompts replaced | -| `engine/steps.ts` | ❌ DELETE | Split into steps-fresh/migrate/update | -| `Tools/migration-v2-to-v3.ts` | ❌ DELETE | Ported to `engine/migrate.ts` | -| `.opencode/PAIOpenCodeWizard.ts` | ❌ DEPRECATE | Ported to `engine/build-opencode.ts` | - ---- - -## 4. Entry Point Flow (Simplified) - -### 4.1 install.sh (15 lines) - -```bash -#!/usr/bin/env bash -set -euo pipefail - -# 1. Check bun -if ! command -v bun &>/dev/null; then - curl -fsSL https://bun.sh/install | bash -fi - -# 2. Launch (GUI default, CLI with --cli flag) -if [ "${1:-}" = "--cli" ]; then - bun PAI-Install/cli/quick-install.ts "${@:2}" -else - cd PAI-Install - bun install --silent - electron . -fi -``` - -### 4.2 Electron Main Process Flow - -```text -Electron Starts - │ - └── detectInstallMode() - │ - ├── "fresh" → loadURL('/flow/fresh') - │ └── 8-Step Fresh Install - │ - ├── "migrate-v2" → loadURL('/flow/migrate') - │ └── 5-Step Migration - │ - ├── "update-v3" → loadURL('/flow/update') - │ └── 3-Step Update - │ - └── "current" → show "Already up to date" -``` - -
-Mermaid detail - -```mermaid -flowchart TD - A["Electron Starts"] --> B["detectInstallMode()"] - B --> C{"Result?"} - C -->|fresh| D["loadURL('/flow/fresh')\n→ 8-Step Fresh Install"] - C -->|migrate-v2| E["loadURL('/flow/migrate')\n→ 5-Step Migration"] - C -->|update-v3| F["loadURL('/flow/update')\n→ 3-Step Update"] - C -->|current| G["Show 'Already up to date'"] -``` - -
- ---- - -## 5. Step Definitions (Updated) - -### 5.1 Fresh Install (8 Steps) - -| Step | UI Screen | Backend Action | Progress | -|------|-----------|----------------|----------| -| 1 | Welcome | Show value prop | 0% | -| 2 | Prerequisites | Check git, bun | 10% | -| 3 | **Build OpenCode** | `engine/build-opencode.ts` | 10-70% | -| | - Clone fork | `git clone Steffen025/opencode` | 20% | -| | - Checkout branch | `git checkout feature/model-tiers` | 30% | -| | - Install deps | `bun install` | 40% | -| | - Build binary | `bun run build.ts --single` | 70% | -| 4 | **AI Provider** ⭐ | Configure API keys | 75% | -| | - **Recommended:** OpenCode Zen (FREE models) | Save `ZEN_API_KEY` | — | -| | - Alternative: Anthropic, OpenRouter | Save respective keys | — | -| 5 | Identity | Save name, AI name, timezone | 85% | -| 6 | Voice (Optional) | ElevenLabs key, test voice | 90% | -| 7 | Install PAI | Copy files, create wrapper | 90-100% | -| 8 | Done | Show summary, launch command | 100% | - -**Step 4 — Provider Selection UI:** -```text -┌─────────────────────────────────────────────────────────┐ -│ │ -│ Step 4 of 8: Choose Your AI Provider │ -│ │ -│ 💚 RECOMMENDED: OpenCode Zen (Start FREE) │ -│ ┌──────────────────────────────────────┐ │ -│ │ │ │ -│ │ 🆓 FREE Tier Available: │ │ -│ │ • MiniMax M2.5 Free — $0 │ │ -│ │ • GPT 5 Nano — $0 │ │ -│ │ • Big Pickle — $0 (limited) │ │ -│ │ │ │ -│ │ Low-cost options: │ │ -│ │ • GPT 5.1 Codex Mini — $0.25/M │ │ -│ │ • Claude Haiku 3.5 — $0.80/M │ │ -│ │ │ │ -│ │ Get your free API key: │ │ -│ │ 👉 https://opencode.ai/zen │ │ -│ │ │ │ -│ │ [I have my Zen API key →] │ │ -│ │ │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ -│ │ -│ 🔄 Use Different Provider: │ -│ • Anthropic (Claude Opus/Sonnet) — Premium quality │ -│ • OpenRouter (Multi-provider) — Flexibility │ -│ • OpenAI (GPT-5 series) — Familiar │ -│ │ -│ [Back] [Continue with Zen] │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - -**Why OpenCode Zen is Default:** -- ✅ FREE tier available (no credit card required) -- ✅ Pay-as-you-go (no subscription) -- ✅ Includes Claude, GPT, and open-source models -- ✅ 60x cost optimization through model tiers -- ✅ Built specifically for PAI-OpenCode workflow - -### 5.2 Migration v2→v3 (5 Steps) - -| Step | UI Screen | Backend Action | Progress | -|------|-----------|----------------|----------| -| 1 | Detected | Show "Found v2.x" | 0% | -| 2 | Backup | `createBackup()` → `~/.opencode-backup-DATE` | 10% | -| 3 | Migrate | `engine/migrate.ts` | 10-70% | -| | - Flatten skills | Move files up one level | 30% | -| | - Update bootstrap | Fix MINIMAL_BOOTSTRAP.md | 50% | -| | - Validate | Run validation checks | 70% | -| 4 | Binary Update | Optional: `build-opencode.ts` | 70-90% | -| 5 | Done | Summary, no settings lost | 100% | - -### 5.3 Update v3→v3.x (3 Steps) - -| Step | UI Screen | Backend Action | Progress | -|------|-----------|----------------|----------| -| 1 | Detected | Show current → new version | 0% | -| 2 | Update | Pull changes, update files | 10-80% | -| 3 | Done | Summary | 100% | - ---- - -## 6. Backend Logic (New Files) - -### 6.1 engine/build-opencode.ts (NEW) - -Ported from `PAIOpenCodeWizard.ts`: - -```typescript -export async function buildOpenCodeBinary( - options: { - onProgress: (step: string, percent: number) => void; - skipIfExists?: boolean; - } -): Promise { - const buildDir = "/tmp/opencode-build-" + Date.now(); - const installPath = "/usr/local/bin/opencode"; - - // Skip if exists - if (options.skipIfExists && existsSync(installPath)) { - return { success: true, skipped: true, version: await getVersion() }; - } - - try { - // Step 1: Clone - options.onProgress("Cloning Steffen025/opencode fork...", 10); - await exec(`git clone https://github.com/Steffen025/opencode.git ${buildDir}`); - - // Step 2: Checkout model-tiers - options.onProgress("Checking out feature/model-tiers...", 30); - await exec(`git checkout feature/model-tiers`, { cwd: buildDir }); - - // Step 3: Install - options.onProgress("Installing dependencies (this takes 2-3 min)...", 50); - await exec(`bun install`, { cwd: buildDir }); - - // Step 4: Build - options.onProgress("Building standalone binary...", 70); - await exec( - `bun run ./packages/opencode/script/build.ts --single`, - { cwd: buildDir } - ); - - // Step 5: Install - options.onProgress("Installing to /usr/local/bin...", 90); - await exec(`cp ${buildDir}/opencode ${installPath}`); - await exec(`chmod +x ${installPath}`); - - options.onProgress("Done!", 100); - return { success: true, version: await getVersion() }; - - } finally { - // Cleanup - await exec(`rm -rf ${buildDir}`); - } -} -``` - -### 6.2 engine/migrate.ts (NEW) - -Ported from `Tools/migration-v2-to-v3.ts`: - -```typescript -export async function migrateV2ToV3( - options: { dryRun?: boolean; onProgress?: (step: string, percent: number) => void } -): Promise { - const paiDir = join(homedir(), ".opencode"); - const backupDir = join(homedir(), `.opencode-backup-${Date.now()}`); - - const result: MigrationResult = { - backedUp: [], - migrated: [], - skipped: [], - errors: [], - }; - - try { - // 1. Backup - options.onProgress?.("Creating backup...", 10); - await createBackup(paiDir, backupDir); - result.backedUp.push(backupDir); - - // 2. Detect flat skills - options.onProgress?.("Detecting flat skill structure...", 20); - const flatSkills = detectFlatSkills(paiDir); - - // 3. Migrate each skill - let progress = 20; - for (const skill of flatSkills) { - options.onProgress?.(`Migrating ${skill}...`, progress); - await migrateFlatSkill(skill); - result.migrated.push(skill); - progress += Math.floor(50 / flatSkills.length); - } - - // 4. Update MINIMAL_BOOTSTRAP.md - options.onProgress?.("Updating bootstrap file...", 80); - await updateMinimalBootstrap(); - - // 5. Validate - options.onProgress?.("Validating migration...", 90); - const validation = await validateMigration(); - if (!validation.valid) { - result.errors.push(...validation.errors); - } - - options.onProgress?.("Migration complete!", 100); - return result; - - } catch (error) { - result.errors.push(error instanceof Error ? error.message : String(error)); - throw error; - } -} -``` - -### 6.3 engine/update.ts (NEW) - -```typescript -export async function updateV3( - currentVersion: string, - targetVersion: string, - options: { onProgress?: (step: string, percent: number) => void } -): Promise { - // 1. Detect what changed - const changes = detectChanges(currentVersion, targetVersion); - - // 2. Apply updates - for (const change of changes) { - await applyChange(change); - } - - // 3. Update version marker - await updateVersionMarker(targetVersion); - - return { success: true, changesApplied: changes.length }; -} -``` - ---- - -## 7. Headless CLI (quick-install.ts) - -### Usage - -```bash -# Fresh install (interactive fallback if no args) -bun PAI-Install/cli/quick-install.ts \ - --preset anthropic \ - --name "Steffen" \ - --ai-name "Jeremy" \ - --timezone "Europe/Berlin" \ - --anthropic-key "sk-..." \ - --elevenlabs-key "..." \ - --build-opencode \ - --voice - -# Migrate -bun PAI-Install/cli/quick-install.ts --migrate --backup-dir ~/backups - -# Update -bun PAI-Install/cli/quick-install.ts --update - -# Dry run (preview) -bun PAI-Install/cli/quick-install.ts --migrate --dry-run -``` - -### Non-Interactive Requirements - -- All required args must be provided (no prompts) -- Progress output to stdout (JSON lines or text) -- Exit code 0 = success, 1 = error -- No TUI, no Electron - ---- - -## 8. UI/UX Design Principles - -### 8.1 One Question Per Screen - -Don't overwhelm users. Each step asks ONE thing: - -```text -┌─────────────────────────────────────────────────────────┐ -│ │ -│ Step 5 of 8 │ -│ │ -│ What's your name? │ -│ │ -│ ┌──────────────────────────────────────┐ │ -│ │ Steffen │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ This will be used to personalize your AI │ -│ assistant's responses. │ -│ │ -│ [Back] [Continue] │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - -### 8.2 Always Show Progress - -Users must know: -- What step they're on -- How many steps total -- What is happening (not just "Loading...") - -```text -Step 3 of 8: Building OpenCode Binary -████████████████████░░░░ 67% - -Current: Compiling TypeScript... -Estimated: 2 minutes remaining -``` - -### 8.3 Explain the "Why" - -When asking for API keys or building binary, explain WHY: - -```text -┌─────────────────────────────────────────────────────────┐ -│ │ -│ Why do you need an Anthropic API key? │ -│ │ -│ PAI-OpenCode uses Claude (via Anthropic API) to │ -│ provide intelligent assistance. Without this, │ -│ the AI features won't work. │ -│ │ -│ Get your key: https://console.anthropic.com │ -│ │ -│ ┌──────────────────────────────────────┐ │ -│ │ sk-ant-... │ │ -│ └──────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - -### 8.4 Skip Option for Advanced Steps - -Building OpenCode takes 3-5 minutes. Allow skipping: - -```text -┌─────────────────────────────────────────────────────────┐ -│ │ -│ ⚙ Building OpenCode │ -│ │ -│ ████████████████████░░ 60% │ -│ │ -│ Compiling... (3-5 minutes total) │ -│ │ -│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ -│ │ -│ [Skip] ← Use standard OpenCode (no model tiers) │ -│ │ -│ (You can build it later by re-running installer) │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 9. Error Handling Strategy - -### 9.1 Recoverable Errors - -| Error | Recovery Action | -|-------|-----------------| -| Git clone fails | Retry with https vs ssh, or manual instructions | -| Bun install fails | Clear cache, retry, or show manual build steps | -| Build fails | Show logs, offer "skip this step" | -| API key invalid | Retry input, link to docs | -| Backup exists | Offer overwrite, append timestamp, or cancel | - -### 9.2 Non-Recoverable Errors - -| Error | Action | -|-------|--------| -| No internet | Show offline instructions | -| Disk full | Show cleanup instructions | -| Permission denied | Show sudo instructions | -| Unknown state | Safe fallback to manual mode | - ---- - -## 10. Testing Strategy - -### 10.1 Test Scenarios - -| Scenario | Test | -|----------|------| -| Fresh macOS install | VM with no bun, no git | -| Fresh Linux install | Ubuntu VM | -| Existing v2 install | Simulate flat skills | -| Existing v3 install | Simulate current version | -| Network failure | Disconnect during build | -| Cancel mid-install | Ctrl+C, resume | -| Headless mode | CI pipeline | - -### 10.2 Automated Tests - -```typescript -// engine/__tests__/detect.test.ts -describe("detectInstallMode", () => { - it("returns 'fresh' when no .opencode exists", () => { - // ... - }); - - it("returns 'migrate-v2' when flat skills detected", () => { - // ... - }); - - it("returns 'update-v3' when v3.x outdated", () => { - // ... - }); -}); -``` - ---- - -## 11. Implementation Tasks (Updated) - -| Task | Effort | Dependencies | -|------|--------|--------------| -| Create `engine/build-opencode.ts` | 1.5h | None | -| Create `engine/migrate.ts` (port from tools/) | 1h | None | -| Create `engine/update.ts` | 30min | None | -| Create `engine/steps-fresh.ts` | 1h | build-opencode.ts | -| Create `engine/steps-migrate.ts` | 45min | migrate.ts | -| Create `engine/steps-update.ts` | 30min | update.ts | -| Simplify `install.sh` (163→15 lines) | 15min | None | -| Create `cli/quick-install.ts` (headless) | 1.5h | All steps-* | -| Update Electron UI for flow routing | 2h | All steps-* | -| ⭐ **Create wrapper script** `/usr/local/bin/{AI_NAME}-wrapper` | 1h | build-opencode.ts | -| ⭐ **Add .zshrc alias integration** | 30min | Wrapper script | -| Delete deprecated files | 15min | All above | -| Write tests | 2h | All above | -| Update documentation | 1h | All above | - -**Total Effort:** ~12.5 hours (added wrapper creation) - ---- - -## 12. Migration from Current State - -### Step-by-Step - -1. **Create new engine files** (parallel to existing) - - `engine/build-opencode.ts` - - `engine/migrate.ts` - - `engine/update.ts` - - `engine/steps-fresh.ts` - - `engine/steps-migrate.ts` - - `engine/steps-update.ts` - -2. **Simplify `install.sh`** - - Reduce to 15 lines - - Test on macOS + Linux - -3. **Create `cli/quick-install.ts`** - - Non-interactive only - - Arg parsing - - Progress output - -4. **Update Electron UI** - - Route based on detectInstallMode() - - Show appropriate flow - -5. **Delete deprecated** - - `cli/display.ts` - - `cli/index.ts` - - `cli/prompts.ts` - - `engine/steps.ts` - - `Tools/migration-v2-to-v3.ts` - - `.opencode/PAIOpenCodeWizard.ts` (add deprecation notice) - -6. **Create wrapper script** ⭐ CRITICAL - - Install to `/usr/local/bin/{AI_NAME}-wrapper` - - Template based on `~/.opencode/tools/opencode-wrapper` - - Install custom binary to `~/.opencode/tools/opencode` - - Add alias to `.zshrc`: `alias {AI_NAME}="{AI_NAME}-wrapper"` - - Include `--rebuild`, `--brew`, `--status` flags - -7. **Test all scenarios** - - Fresh install - - Migrate v2→v3 - - Update v3→v3.x - - Headless mode - - **Wrapper test:** Type `{AI_NAME}` after restart → must use custom build - - **Brew escape:** `{AI_NAME} --brew` → must use Homebrew version - ---- - -## 13. Post-Refactor Verification - -### Checklist - -- [ ] `install.sh` is <20 lines -- [ ] Only ONE entry point (Electron GUI) -- [ ] Headless mode works (`--cli` flag) -- [ ] Auto-detect works for fresh/migrate/update -- [ ] Build OpenCode step shows progress -- [ ] Migration creates backup before changing -- [ ] Update preserves settings -- [ ] **Wrapper created at** `/usr/local/bin/{AI_NAME}-wrapper` -- [ ] **Custom binary at** `~/.opencode/tools/opencode` -- [ ] **Alias in .zshrc** works after restart -- [ ] `{AI_NAME}` command uses custom build (not Homebrew) -- [ ] `{AI_NAME} --brew` escape hatch works -- [ ] `{AI_NAME} --rebuild` rebuilds from source -- [ ] `{AI_NAME} --status` shows build info -- [ ] All scenarios tested -- [ ] Documentation updated - -### Wrapper Test Procedure - -```bash -# 1. Test fresh install -bash PAI-Install/install.sh -# Complete installation... - -# 2. Verify wrapper exists -which {AI_NAME} -# Should output: /usr/local/bin/{AI_NAME}-wrapper - -# 3. Verify alias in .zshrc -grep "alias {AI_NAME}" ~/.zshrc -# Should show: alias {AI_NAME}="/usr/local/bin/{AI_NAME}-wrapper" - -# 4. Test wrapper uses custom build -{AI_NAME} --status -# Should show: Binary: /Users/.../.opencode/tools/opencode -# Should show: Branch: feature/model-tiers - -# 5. Simulate restart (new shell) -exec zsh -{AI_NAME} --status -# Should STILL show custom build (not Homebrew) - -# 6. Test escape hatch -{AI_NAME} --brew --version -# Should show Homebrew version - -# 7. Test rebuild -{AI_NAME} --rebuild -# Should rebuild from source -``` - ---- - -## 14. Clarifications Summary - -### What We Learned from PR #47 - -1. **The installer does TWO things:** Build OpenCode binary + Install PAI files -2. **Users are confused** by 4 entry points — need ONE -3. **Build takes 3-5 min** — must show progress + allow skip -4. **Migration is separate** — must integrate into installer -5. **Headless mode needed** — for CI/homeserver users -6. **Auto-detect is key** — don't make users choose - -### Clarifications from Jeremy (2026-03-09) - -#### Q1: Should we bundle OpenCode binary or always build from source? - -**Answer:** Always build from source — because: -- Standard OpenCode (brew install) lacks model-tiers feature -- Custom build needed for dynamic routing (quick/standard/advanced) -- Can't upload binaries to GitHub (size limits) -- Build is now Bun-based (reliable, no Go needed) - -**Solution:** Build during install with clear progress UI + skip option - ---- - -#### Q2: API Key Strategy — No Anthropic Key Required! - -**Key Insight:** Since we install OpenCode (not Claude Code PAI), users DON'T need Anthropic API key! - -**Revised Provider Flow:** - -**Step 1: Direct users to OpenCode-Zen (FREE option)** -- URL: https://opencode.ai/docs/zen/ -- Models available: - - **MiniMax M2.5 Free** — FREE (limited time) - - **Big Pickle** — FREE (limited time, stealth model) - - **GPT 5 Nano** — FREE - - **GPT 5.1 Codex Mini** — $0.25/$2.00 per 1M tokens -- Get key at: https://opencode.ai/zen - -**Step 2: Alternative API Keys (optional)** -- **Anthropic** — for Claude users (Opus 4.6, Sonnet 4.6, etc.) -- **OpenRouter** — for multi-provider access -- **OpenAI** — for GPT models - -**UI Design:** -```text -┌─────────────────────────────────────────────────────────┐ -│ │ -│ Step 4 of 8: Choose Your AI Provider │ -│ │ -│ 💡 RECOMMENDED: OpenCode Zen (FREE) │ -│ ┌──────────────────────────────────────┐ │ -│ │ • MiniMax M2.5 Free — $0 │ │ -│ │ • GPT 5 Nano — $0 │ │ -│ │ • GPT 5.1 Codex Mini — $0.25/M │ │ -│ │ │ │ -│ │ Get free key: opencode.ai/zen │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ -│ │ -│ Other Options: │ -│ ┌──────────────────────────────────────┐ │ -│ │ Anthropic (Claude) — $3-15/M tokens │ │ -│ │ OpenRouter (Multi-provider) │ │ -│ │ OpenAI (GPT-4/5) │ │ -│ └──────────────────────────────────────┘ │ -│ │ -│ [Back] [Continue with Zen] │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -#### Q3: What if build fails? - -**Answer:** Build rarely fails (Bun-based, reliable), but if it does: - -**Recovery Options:** -1. **Show detailed error logs** in UI -2. **Offer "Try Again"** — most network issues are transient -3. **Manual build instructions** — fallback for advanced users -4. **Skip option** — use standard OpenCode (no model tiers) - -**Note:** Cannot offer pre-built binary download due to GitHub size limits - ---- - -#### Q4: Update Frequency? - -**Answer:** Check on EVERY launch - -**Implementation:** Custom wrapper command (like "jeremy") - -**Current Setup (reference implementation - `~/.opencode/tools/opencode-wrapper`):** - -```bash -#!/usr/bin/env bash -# -# WHY: The Homebrew build of OpenCode doesn't support our custom agent system -# (model_tiers, agent frontmatter metadata, PAI CODE branding). We compile our -# own binary from the feature/model-tiers branch. -# -# The compiled binary runs from ANY directory - no --cwd tricks, no symlinks, -# no process.cwd() overrides needed. - -OPENCODE_SRC="/Users/steffen/workspace/github.com/anomalyco/opencode" -PAI_BIN="${HOME}/.opencode/tools/opencode" -BREW_BIN="/usr/local/bin/opencode" - -# Rebuild from source -rebuild() { - echo "[PAI CODE] Rebuilding from source..." - - # Build - (cd "${OPENCODE_SRC}" && bun run --filter=opencode build) - - # Symlink binary (Bun-compiled binaries MUST stay in dist/) - local dist_bin="${OPENCODE_SRC}/packages/opencode/dist/opencode-darwin-arm64/bin/opencode" - rm -f "${PAI_BIN}" - ln -s "${dist_bin}" "${PAI_BIN}" - - echo "[PAI CODE] Build complete!" -} - -# Show status -show_status() { - local branch=$(cd "${OPENCODE_SRC}" && git branch --show-current) - local commit=$(cd "${OPENCODE_SRC}" && git log --oneline -1) - local binary_exists=$([[ -f "${PAI_BIN}" ]] && echo "yes" || echo "NO") - - echo "PAI CODE - Custom Build Status" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Binary: ${PAI_BIN}" - echo "Binary exists: ${binary_exists}" - echo "Source: ${OPENCODE_SRC}" - echo "Branch: ${branch}" - echo "Latest commit: ${commit}" - echo "" - echo "Custom features:" - echo " - Agent model_tier support (quick/standard/advanced)" - echo " - Agent frontmatter metadata (voice, fallback, etc.)" - echo " - PAI CODE branding" -} - -# Main -main() { - case "${1:-}" in - --status) - show_status - exit 0 - ;; - --brew) - shift - echo "[PAI CODE] Using Homebrew version (escape hatch)..." - exec "${BREW_BIN}" "$@" - ;; - --rebuild) - rebuild - exit $? - ;; - esac - - # Verify binary exists - if [[ ! -f "${PAI_BIN}" ]]; then - echo "[PAI CODE] Binary not found. Run: opencode-wrapper --rebuild" - echo "[PAI CODE] Falling back to Homebrew..." - exec "${BREW_BIN}" "$@" - fi - - # Run our custom binary - exec "${PAI_BIN}" "$@" -} - -main "$@" -``` - -**Called from `.zshrc`:** -```bash -jeremy() { - cd ~/workspace/github.com/Steffen025/jeremy-opencode && ~/.opencode/tools/opencode-wrapper "$@" -} -``` - -**Key Features:** -- ✅ Checks if custom build exists -- ✅ Falls back to Homebrew if missing -- ✅ `--rebuild` flag to rebuild from source -- ✅ `--brew` escape hatch to use Homebrew -- ✅ `--status` shows build info -- ✅ Works from any directory -- ✅ Bun-compiled binary stays in dist/ (symlinked, not copied) - ---- - -**For Installer: Create similar solution** - -```bash -# After install, user's .zshrc gets: -alias {AI_NAME}="/usr/local/bin/{AI_NAME}-wrapper" - -# Wrapper script at /usr/local/bin/{AI_NAME}-wrapper: -# - Checks custom binary at ~/.opencode/tools/opencode -# - Compares version/hash -# - Rebuilds if outdated -# - Launches correct binary -``` - -**Critical Problem to Solve:** -> "When users type 'opencode' after restart, it loads standard OpenCode (brew) instead of our custom build" - -**Solution (from reference implementation):** -1. **Install custom binary to** `~/.opencode/tools/opencode` (NOT /usr/local/bin) -2. **Create wrapper script** at `/usr/local/bin/{AI_NAME}` -3. **Wrapper ensures correct binary** is always used -4. **Escape hatch**: `--brew` flag for standard OpenCode -5. **Custom logos and branding** preserved in custom build - ---- - -#### Q5: Should migration be automatic? - -**Answer:** NO — Migration must be EXPLICIT with user confirmation - -**Migration Flow:** -```text -┌─────────────────────────────────────────────────────────┐ -│ │ -│ ⚠️ Migration Required │ -│ │ -│ We found PAI-OpenCode v2.x at: │ -│ ~/.opencode │ -│ │ -│ What will happen: │ -│ • Backup created: ~/.opencode-backup-20260309 │ -│ • Skills reorganized (flat → hierarchical) │ -│ • Settings preserved │ -│ • ~5 minutes duration │ -│ │ -│ ⬇️ BEFORE PROCEEDING: │ -│ Your data will be backed up automatically. │ -│ You can restore from backup if anything goes wrong │ -│ │ -│ [Cancel] [Create Backup & Migrate] │ -│ │ -│ ℹ️ Learn more: docs/MIGRATION.md │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - -**Requirements:** -1. **Explicit user consent** — no automatic migration -2. **Backup created FIRST** — before any changes -3. **Clear explanation** — what will happen, how long it takes -4. **Cancel option** — user can abort anytime -5. **Restore instructions** — documented for emergencies - ---- - -### API Key Security Strategy (Q2 Detailed) - -**Options Considered:** - -| Option | Pros | Cons | Recommendation | -|--------|------|------|----------------| -| **Electron secure storage** | OS keychain integration | Complex, platform-specific | USE for production | -| **~/.opencode/.env file** | Simple, accessible | Plain text (chmod 600) | USE for dev/CI | -| **Environment variable** | Standard, flexible | Not persistent across sessions | Alternative | -| **settings.json** | Centralized | Plain text, version controlled | NOT recommended | - -**Recommended Implementation:** - -1. **Electron GUI:** Use `safeStorage` API (encrypts with OS keychain) -2. **Headless/CLI:** Use `~/.opencode/.env` with 0600 permissions -3. **Migration:** Preserve existing keys, re-encrypt if needed - -**Code Example:** -```typescript -// engine/config-gen.ts -export async function saveApiKey(provider: string, key: string): Promise { - const envPath = join(homedir(), ".opencode", ".env"); - - // Electron: Use secure storage - if (isElectron()) { - const encrypted = await safeStorage.encryptString(key); - await writeFile(`${envPath}.${provider}.enc`, encrypted, { mode: 0o600 }); - } else { - // CLI: Plain env file with restricted permissions - await appendFile(envPath, `${provider}_API_KEY=${key}\n`); - await chmod(envPath, 0o600); - } -} -``` - ---- - -### OpenCode-Zen Model Configuration - -**For settings.json:** - -```json -{ - "models": { - "defaultProvider": "opencode-zen", - "providers": { - "opencode-zen": { - "baseURL": "https://opencode.ai/zen/v1", - "models": { - "quick": "minimax-m2.5-free", // FREE - "standard": "gpt-5.1-codex-mini", // $0.25/M - "advanced": "claude-sonnet-4-6" // $3.00/M - } - } - } - } -} -``` - -**Free Tier Limits:** -- MiniMax M2.5 Free: Rate limited, feedback collection period -- Big Pickle: Stealth model, limited availability -- GPT 5 Nano: Always free - -**Paid Tier:** Pay-as-you-go, no subscription - ---- - -## 15. Next Steps - -1. **✅ Questions clarified** (see §14) -2. **Create feature branch:** `feature/wp-e-installer-refactor` -3. **Implement in order:** §11 tasks -4. **Test all scenarios** -5. **Create PR #48** -6. **Merge to dev** - ---- - -*Updated: 2026-03-09 (after PR #47 merge + Jeremy clarifications)* -*Status: Ready for implementation* -*Target: PR #48* From 2bd381cea27689820cae9c0aed1e1c8ed3fc98eb Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:11:27 -0400 Subject: [PATCH 6/8] fix: replace all hardcoded ElevenLabs voice IDs with dynamic routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VoiceServer sendNotification() now uses title (agent name) instead of voiceId for voice resolution. This enables dynamic provider switching via TTS_PROVIDER env var (google/elevenlabs/macos). Changed: voice_id:'fTtv3eikoepIosk8dTZ5' → voice_id:'default' + title:'AgentName' Files affected: - VoiceServer/server.ts (line 425: voiceId → safeTitle) - PAI/SKILL.md (7 phase curls) - PAI/Algorithm/v3.7.0.md (2 voice curls) - agents/Algorithm.md (frontmatter + 3 references) - PAI/Tools/algorithm.ts (VOICE_ID constant) Zero hardcoded ElevenLabs IDs remain in the codebase. --- .opencode/PAI/Algorithm/v3.7.0.md | 4 ++-- .opencode/PAI/SKILL.md | 14 +++++++------- .opencode/PAI/Tools/algorithm.ts | 2 +- .opencode/VoiceServer/server.ts | 3 ++- .opencode/agents/Algorithm.md | 8 ++++---- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.opencode/PAI/Algorithm/v3.7.0.md b/.opencode/PAI/Algorithm/v3.7.0.md index e326c9ea..a492474b 100644 --- a/.opencode/PAI/Algorithm/v3.7.0.md +++ b/.opencode/PAI/Algorithm/v3.7.0.md @@ -25,7 +25,7 @@ At Algorithm entry and every phase transition, announce via direct inline curl ( ```bash curl -s -X POST http://localhost:8888/notify \ -H "Content-Type: application/json" \ - -d '{"message": "MESSAGE", "voice_id": "fTtv3eikoepIosk8dTZ5", "voice_enabled": true}' + -d '{"message": "MESSAGE", "voice_id": "default", "title": "Jeremy", "voice_enabled": true}' ``` **Algorithm entry:** `"Entering the Algorithm"` — immediately before OBSERVE begins. @@ -121,7 +121,7 @@ The coarse version has 3 criteria that each hide 6+ verifiable sub-requirements. 🗒️ TASK: [8 word description] ``` -**Voice (FIRST action after loading this file):** `curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"message": "Entering the Algorithm", "voice_id": "fTtv3eikoepIosk8dTZ5", "voice_enabled": true}'` +**Voice (FIRST action after loading this file):** `curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"message": "Entering the Algorithm", "voice_id": "default", "title": "Jeremy", "voice_enabled": true}'` **PRD stub (MANDATORY — immediately after voice curl):** Create the PRD directory and write a stub PRD with frontmatter only. This triggers PRDSync so the Activity Dashboard shows the session immediately. diff --git a/.opencode/PAI/SKILL.md b/.opencode/PAI/SKILL.md index b30edea4..4087d45f 100644 --- a/.opencode/PAI/SKILL.md +++ b/.opencode/PAI/SKILL.md @@ -238,7 +238,7 @@ More ISC = finer verification = better hill-climbing. When in doubt, more criter 🗒️ TASK: [8 word description] -`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"fTtv3eikoepIosk8dTZ5","message": "Entering the PAI Algorithm Observe phase"}'` +`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"default","title":"Jeremy","message": "Entering the PAI Algorithm Observe phase"}'` ━━━ 👁️ OBSERVE ━━━ 1/7 ``` @@ -270,7 +270,7 @@ Walk the Full Capability Registry (25 capabilities, Sections A-F) and assign USE **Quality Gate → OPEN or BLOCKED.** ``` -`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"fTtv3eikoepIosk8dTZ5","message": "Entering the Think phase"}'` +`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"default","title":"Jeremy","message": "Entering the Think phase"}'` ━━━ 🧠 THINK ━━━ 2/7 ``` @@ -286,7 +286,7 @@ Walk the Full Capability Registry (25 capabilities, Sections A-F) and assign USE Extended+: Rehearse verification for each CRITICAL criterion. ``` -`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"fTtv3eikoepIosk8dTZ5","message": "Entering the Plan phase"}'` +`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"default","title":"Jeremy","message": "Entering the Plan phase"}'` ━━━ 📋 PLAN ━━━ 3/7 ``` @@ -299,7 +299,7 @@ Extended+: Rehearse verification for each CRITICAL criterion. - Quality Gate re-check. ``` -`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"fTtv3eikoepIosk8dTZ5","message": "Entering the Build phase"}'` +`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"default","title":"Jeremy","message": "Entering the Build phase"}'` ━━━ 🔨 BUILD ━━━ 4/7 ``` @@ -309,7 +309,7 @@ Extended+: Rehearse verification for each CRITICAL criterion. - Create artifacts. Log work and observations to PRD. ``` -`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"fTtv3eikoepIosk8dTZ5","message": "Entering the Execute phase"}'` +`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"default","title":"Jeremy","message": "Entering the Execute phase"}'` ━━━ ⚡ EXECUTE ━━━ 5/7 ``` @@ -320,7 +320,7 @@ Extended+: Rehearse verification for each CRITICAL criterion. - Log work and observations to PRD. ``` -`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"fTtv3eikoepIosk8dTZ5","message": "Entering the Verify phase."}'` +`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"default","title":"Jeremy","message": "Entering the Verify phase."}'` ━━━ ✅ VERIFY ━━━ 6/7 ``` @@ -337,7 +337,7 @@ Extended+: Rehearse verification for each CRITICAL criterion. - Clear ISC/VERIFICATION TaskList. ``` -`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"fTtv3eikoepIosk8dTZ5","message": "Entering the Learn phase"}'` +`curl -s -X POST http://localhost:8888/notify -H "Content-Type: application/json" -d '{"voice_id":"default","title":"Jeremy","message": "Entering the Learn phase"}'` ━━━ 📚 LEARN ━━━ 7/7 ``` diff --git a/.opencode/PAI/Tools/algorithm.ts b/.opencode/PAI/Tools/algorithm.ts index 5567bffb..950cb0ef 100644 --- a/.opencode/PAI/Tools/algorithm.ts +++ b/.opencode/PAI/Tools/algorithm.ts @@ -51,7 +51,7 @@ const ALGORITHMS_DIR = join(BASE_DIR, "MEMORY", "STATE", "algorithms"); const SESSION_NAMES_PATH = join(BASE_DIR, "MEMORY", "STATE", "session-names.json"); const PROJECTS_DIR = process.env.PROJECTS_DIR || join(HOME, "Projects"); const VOICE_URL = "http://localhost:8888/notify"; -const VOICE_ID = "fTtv3eikoepIosk8dTZ5"; +const VOICE_ID = "default"; // ─── Types ────────────────────────────────────────────────────────────────── diff --git a/.opencode/VoiceServer/server.ts b/.opencode/VoiceServer/server.ts index fd4ff967..7ac5e847 100644 --- a/.opencode/VoiceServer/server.ts +++ b/.opencode/VoiceServer/server.ts @@ -422,7 +422,8 @@ async function sendNotification( if (voiceEnabled && apiKeyConfigured) { try { const voiceConfig = voiceId ? getVoiceConfig(voiceId) : null; - const agentName = voiceConfig?.voice_name || voiceId; + // Use title (agent name) for voice resolution, falling back to voice config name + const agentName = voiceConfig?.voice_name || safeTitle; const voice = resolveVoiceId(voiceConfig?.voice_id || voiceId, agentName); // Determine voice settings (priority: emotional > personality > defaults) diff --git a/.opencode/agents/Algorithm.md b/.opencode/agents/Algorithm.md index 040cb06b..f14d0284 100644 --- a/.opencode/agents/Algorithm.md +++ b/.opencode/agents/Algorithm.md @@ -3,7 +3,7 @@ name: Algorithm description: Expert in creating and evolving Ideal State Criteria (ISC) as part of the PAI Algorithm's core principles. Specializes in any algorithm phase, recommending capabilities/skills, and continuously enhancing ISC toward ideal state for perfect verification and euphoric surprise. model: opus color: "#3B82F6" -voiceId: fTtv3eikoepIosk8dTZ5 +voiceId: default voice: stability: 0.65 similarity_boost: 0.86 @@ -40,7 +40,7 @@ permissions: ```bash curl -X POST http://localhost:8888/notify \ -H "Content-Type: application/json" \ - -d '{"message":"Algorithm agent activated, loading ISC expertise","voice_id":"fTtv3eikoepIosk8dTZ5","title":"Algorithm Agent"}' + -d '{"message":"Algorithm agent activated, loading ISC expertise","voice_id":"default","title":"Algorithm Agent"}' ``` 2. **Load your knowledge base:** @@ -82,11 +82,11 @@ You embody the PAI Algorithm's core philosophy: ```bash curl -X POST http://localhost:8888/notify \ -H "Content-Type: application/json" \ - -d '{"message":"Your COMPLETED line content here","voice_id":"fTtv3eikoepIosk8dTZ5","title":"Algorithm Agent"}' + -d '{"message":"Your COMPLETED line content here","voice_id":"default","title":"Algorithm Agent"}' ``` **Voice Requirements:** -- Your voice_id is: `fTtv3eikoepIosk8dTZ5` +- Your voice_id is: `default` (resolved dynamically by VoiceServer) - Message should be your 🎯 COMPLETED line (8-16 words optimal) - Must be grammatically correct and speakable - Send BEFORE writing your response From 5655797e5f269e3044fbc39de32f0db268c4bda5 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:21:30 -0400 Subject: [PATCH 7/8] fix: address CodeRabbit review findings on PR #83 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes verified against actual code: Tools path resolution: - SkillSearch.ts: INDEX_FILE pointed to PAI/Skills/ instead of skills/ - ValidateSkillStructure.ts: SKILLS_DIR went 3 levels up instead of 2 - Both now match GenerateSkillIndex.ts (.opencode/skills/) Symlink cycle prevention: - GenerateSkillIndex.ts: findSkillFiles now tracks visited canonical paths - ValidateSkillStructure.ts: scanDirectory now populates visitedPaths Set Error handling: - GenerateSkillIndex.ts + SkillSearch.ts: main().catch now exits non-zero Documentation accuracy: - doc-dependencies.json: add missing THEPLUGINSYSTEM.md entry - THEHOOKSYSTEM.md: PAI_DIR example $HOME/.claude → $HOME/.opencode - THEPLUGINSYSTEM.md: library count 8 → 9 (matches table) - pronunciations.json: stale skills/PAI/USER/ → PAI/USER/ path Stats accuracy: - ValidateSkillStructure.ts: category SKILL.md files no longer counted as flat skills - pai-voice.5s.sh: PAI_DIR default $HOME/.claude → $HOME/.opencode Skipped (verified not applicable): - PIPELINES.md paths: upstream semantics, both paths correct in context - VoiceServer shell script hardening: upstream 4.0.3 files, out of scope - PAISECURITYSYSTEM/HOOKS.md: doc is factually accurate for hook architecture - THEHOOKSYSTEM.md voiceId examples: settings.json config docs, not hardcoded IDs --- .opencode/PAI/THEHOOKSYSTEM.md | 2 +- .opencode/PAI/THEPLUGINSYSTEM.md | 2 +- .opencode/PAI/Tools/GenerateSkillIndex.ts | 18 ++++++++++--- .opencode/PAI/Tools/SkillSearch.ts | 7 +++-- .opencode/PAI/Tools/ValidateSkillStructure.ts | 26 +++++++++++++++---- .opencode/PAI/doc-dependencies.json | 6 +++++ .opencode/VoiceServer/menubar/pai-voice.5s.sh | 2 +- .opencode/VoiceServer/pronunciations.json | 2 +- 8 files changed, 50 insertions(+), 15 deletions(-) diff --git a/.opencode/PAI/THEHOOKSYSTEM.md b/.opencode/PAI/THEHOOKSYSTEM.md index b46c2bc9..ea91c190 100755 --- a/.opencode/PAI/THEHOOKSYSTEM.md +++ b/.opencode/PAI/THEHOOKSYSTEM.md @@ -357,7 +357,7 @@ Hooks have access to all environment variables from `~/.opencode/settings.json` ```json { "env": { - "PAI_DIR": "$HOME/.claude", + "PAI_DIR": "$HOME/.opencode", "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000" } } diff --git a/.opencode/PAI/THEPLUGINSYSTEM.md b/.opencode/PAI/THEPLUGINSYSTEM.md index ddaa3e22..42c66f64 100644 --- a/.opencode/PAI/THEPLUGINSYSTEM.md +++ b/.opencode/PAI/THEPLUGINSYSTEM.md @@ -245,7 +245,7 @@ OpenCode Bash is **stateless** — every call spawns a fresh process. This hook --- -## Library Reference (8 Libraries) +## Library Reference (9 Libraries) | Library | Purpose | |---------|---------| diff --git a/.opencode/PAI/Tools/GenerateSkillIndex.ts b/.opencode/PAI/Tools/GenerateSkillIndex.ts index 5f230f0e..a8ffdc2d 100644 --- a/.opencode/PAI/Tools/GenerateSkillIndex.ts +++ b/.opencode/PAI/Tools/GenerateSkillIndex.ts @@ -10,7 +10,7 @@ * Output: ~/.opencode/skills/skill-index.json */ -import { readdir, readFile, writeFile, stat } from 'fs/promises'; +import { readdir, readFile, writeFile, stat, realpath } from 'fs/promises'; import { join, relative, sep } from 'path'; import { existsSync } from 'fs'; @@ -49,10 +49,17 @@ const ALWAYS_LOADED_SKILLS = [ 'Art', ]; -async function findSkillFiles(dir: string): Promise { +async function findSkillFiles(dir: string, visited: Set = new Set()): Promise { const skillFiles: string[] = []; try { + // Track canonical path to prevent symlink cycles + const canonical = await realpath(dir); + if (visited.has(canonical)) { + return skillFiles; + } + visited.add(canonical); + const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { @@ -83,7 +90,7 @@ async function findSkillFiles(dir: string): Promise { } // Recurse into subdirectories (including symlinked ones) - const nestedFiles = await findSkillFiles(fullPath); + const nestedFiles = await findSkillFiles(fullPath, visited); skillFiles.push(...nestedFiles); } } @@ -371,4 +378,7 @@ async function main() { } } -main().catch(console.error); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/.opencode/PAI/Tools/SkillSearch.ts b/.opencode/PAI/Tools/SkillSearch.ts index 2914852f..96d72a4d 100644 --- a/.opencode/PAI/Tools/SkillSearch.ts +++ b/.opencode/PAI/Tools/SkillSearch.ts @@ -19,7 +19,7 @@ import { readFile } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; -const INDEX_FILE = join(import.meta.dir, '..', 'Skills', 'skill-index.json'); +const INDEX_FILE = join(import.meta.dir, '..', '..', 'skills', 'skill-index.json'); interface SkillEntry { name: string; @@ -200,4 +200,7 @@ async function main() { } } -main().catch(console.error); +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/.opencode/PAI/Tools/ValidateSkillStructure.ts b/.opencode/PAI/Tools/ValidateSkillStructure.ts index ffc9d067..14958477 100644 --- a/.opencode/PAI/Tools/ValidateSkillStructure.ts +++ b/.opencode/PAI/Tools/ValidateSkillStructure.ts @@ -19,7 +19,7 @@ import { readdir, readFile, stat, realpath } from 'fs/promises'; import { join, relative, sep } from 'path'; import { existsSync } from 'fs'; -const SKILLS_DIR = join(import.meta.dir, '..', '..', '..', 'skills'); +const SKILLS_DIR = join(import.meta.dir, '..', '..', 'skills'); interface ValidationIssue { type: 'error' | 'warning'; @@ -50,6 +50,13 @@ async function validateSkillStructure(): Promise { async function scanDirectory(dir: string, depth: number = 0, visitedPaths: Set = new Set()): Promise { try { + // Track canonical path of current directory to prevent cycles + const dirCanonical = await realpath(dir); + if (visitedPaths.has(dirCanonical)) { + return; + } + visitedPaths.add(dirCanonical); + const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { @@ -69,7 +76,6 @@ async function validateSkillStructure(): Promise { }); continue; } - // Will be processed below; canonical path added before recursion } catch (err) { // Report broken symlinks as structural errors issues.push({ @@ -100,9 +106,19 @@ async function validateSkillStructure(): Promise { const pathParts = relativePath.split(sep); if (pathParts.length === 1) { - // Flat skill: skills/SkillName/ - flatSkills++; - await validateSkill(skillMdPath, relativePath, issues, skillNames); + // Check if this is a category directory (has subdirs with SKILL.md) + const subEntries = await readdir(fullPath, { withFileTypes: true }); + const hasChildSkills = subEntries.some(e => + e.isDirectory() && existsSync(join(fullPath, e.name, 'SKILL.md')) + ); + if (hasChildSkills) { + // Category metadata SKILL.md — register as category, don't count as skill + categories.add(entry.name); + } else { + // Flat skill: skills/SkillName/ + flatSkills++; + await validateSkill(skillMdPath, relativePath, issues, skillNames); + } } else if (pathParts.length === 2) { // Hierarchical skill: skills/Category/SkillName/ hierarchicalSkills++; diff --git a/.opencode/PAI/doc-dependencies.json b/.opencode/PAI/doc-dependencies.json index b1c5a634..af21077c 100644 --- a/.opencode/PAI/doc-dependencies.json +++ b/.opencode/PAI/doc-dependencies.json @@ -101,6 +101,12 @@ "upstream": "PAISYSTEMARCHITECTURE.md" }, + "THEPLUGINSYSTEM.md": { + "description": "Plugin system documentation (OpenCode adaptation of Hook system, see ADR-001)", + "tier": 2, + "upstream": "PAISYSTEMARCHITECTURE.md" + }, + "PAIAGENTSYSTEM.md": { "description": "Agent system detailed documentation", "tier": 2, diff --git a/.opencode/VoiceServer/menubar/pai-voice.5s.sh b/.opencode/VoiceServer/menubar/pai-voice.5s.sh index d799df58..2a3b4760 100755 --- a/.opencode/VoiceServer/menubar/pai-voice.5s.sh +++ b/.opencode/VoiceServer/menubar/pai-voice.5s.sh @@ -4,7 +4,7 @@ # For BitBar/SwiftBar - updates every 5 seconds # Get the VoiceServer directory -PAI_DIR="${PAI_DIR:-$HOME/.claude}" +PAI_DIR="${PAI_DIR:-$HOME/.opencode}" VOICE_SERVER_DIR="$PAI_DIR/VoiceServer" # Check if server is running diff --git a/.opencode/VoiceServer/pronunciations.json b/.opencode/VoiceServer/pronunciations.json index 5d32e711..a86f9621 100644 --- a/.opencode/VoiceServer/pronunciations.json +++ b/.opencode/VoiceServer/pronunciations.json @@ -1,5 +1,5 @@ { - "_comment": "TTS pronunciation overrides. Each entry maps a display term to its phonetic TTS spelling. Uses word-boundary matching to avoid mid-word replacements. Source of truth: skills/PAI/USER/PRONUNCIATIONS.md", + "_comment": "TTS pronunciation overrides. Each entry maps a display term to its phonetic TTS spelling. Uses word-boundary matching to avoid mid-word replacements. Source of truth: PAI/USER/PRONUNCIATIONS.md", "replacements": [ { "term": "PAI", "phonetic": "pie", "note": "Personal AI Infrastructure - rhymes with sky" }, { "term": "ISC", "phonetic": "I S C", "note": "Ideal State Criteria - spell out" } From 2612021950f455a008aa5f6c05f648956c0e5f0d Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:36:35 -0400 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20delete=20THEHOOKSYSTEM.md=20and=20HO?= =?UTF-8?q?OKS.md,=20complete=20hook=E2=86=92plugin=20migration=20(ADR-001?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete .opencode/PAI/THEHOOKSYSTEM.md (superseded by THEPLUGINSYSTEM.md) - Delete .opencode/PAISECURITYSYSTEM/HOOKS.md (superseded by PLUGINS.md) - Update doc-dependencies.json: remove THEHOOKSYSTEM.md entry, rename hook-system section to plugin-system referencing THEPLUGINSYSTEM.md - Update PAISECURITYSYSTEM/ARCHITECTURE.md: all hook→plugin terminology, execution flow updated for plugin model (throw Error, not exit codes) - Update PAISECURITYSYSTEM/README.md: hook→plugin in prose and file tree - Fix GenerateSkillIndex.ts division by zero when totalSkills == 0 --- .opencode/PAI/THEHOOKSYSTEM.md | 1327 ------------------- .opencode/PAI/Tools/GenerateSkillIndex.ts | 4 +- .opencode/PAI/doc-dependencies.json | 12 +- .opencode/PAISECURITYSYSTEM/ARCHITECTURE.md | 32 +- .opencode/PAISECURITYSYSTEM/HOOKS.md | 254 ---- .opencode/PAISECURITYSYSTEM/README.md | 10 +- 6 files changed, 26 insertions(+), 1613 deletions(-) delete mode 100755 .opencode/PAI/THEHOOKSYSTEM.md delete mode 100644 .opencode/PAISECURITYSYSTEM/HOOKS.md diff --git a/.opencode/PAI/THEHOOKSYSTEM.md b/.opencode/PAI/THEHOOKSYSTEM.md deleted file mode 100755 index ea91c190..00000000 --- a/.opencode/PAI/THEHOOKSYSTEM.md +++ /dev/null @@ -1,1327 +0,0 @@ -# Hook System - -> **PAI 4.0** — This system is under active development. APIs, configuration formats, and features may change without notice. - -**Event-Driven Automation Infrastructure** - -**Location:** `~/.opencode/hooks/` -**Configuration:** `~/.opencode/settings.json` -**Status:** Active - 20 hooks running in production - ---- - -## Overview - -The PAI hook system is an event-driven automation infrastructure built on Claude Code's native hook support. Hooks are executable scripts (TypeScript/Python) that run automatically in response to specific events during Claude Code sessions. - -**Core Capabilities:** -- **Session Management** - Auto-load context, capture summaries, manage state -- **Voice Notifications** - Text-to-speech announcements for task completions -- **History Capture** - Automatic work/learning documentation to `~/.opencode/MEMORY/` -- **Multi-Agent Support** - Agent-specific hooks with voice routing -- **Tab Titles** - Dynamic terminal tab updates with task context -- **Unified Event Stream** - All hooks emit structured events to `events.jsonl` for real-time observability - -**Key Principle:** Hooks run asynchronously and fail gracefully. They enhance the user experience but never block Claude Code's core functionality. - ---- - -## Available Hook Types - -Claude Code supports the following hook events: - -### 1. **SessionStart** -**When:** Claude Code session begins (new conversation) -**Use Cases:** -- Load PAI context from `PAI/SKILL.md` -- Initialize session state -- Capture session metadata - -**Current Hooks:** -```json -{ - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "${PAI_DIR}/hooks/KittyEnvPersist.hook.ts" - }, - { - "type": "command", - "command": "${PAI_DIR}/hooks/LoadContext.hook.ts" - } - ] - } - ] -} -``` - -**What They Do:** -- `KittyEnvPersist.hook.ts` - Persists Kitty terminal env vars to disk and resets tab title to clean state -- `LoadContext.hook.ts` - Injects dynamic context (relationship, learning, work summary) as `` at session start - ---- - -### 2. **SessionEnd** -**When:** Claude Code session terminates (conversation ends) -**Use Cases:** -- Capture work completions and learning moments -- Generate session summaries -- Record relationship context -- Update system counts (skills, hooks, signals) -- Run integrity checks - -**Current Hooks:** -```json -{ - "SessionEnd": [ - { - "hooks": [ - { - "type": "command", - "command": "${PAI_DIR}/hooks/WorkCompletionLearning.hook.ts" - }, - { - "type": "command", - "command": "${PAI_DIR}/hooks/SessionCleanup.hook.ts" - }, - { - "type": "command", - "command": "${PAI_DIR}/hooks/RelationshipMemory.hook.ts" - }, - { - "type": "command", - "command": "${PAI_DIR}/hooks/UpdateCounts.hook.ts" - }, - { - "type": "command", - "command": "${PAI_DIR}/hooks/IntegrityCheck.hook.ts" - } - ] - } - ] -} -``` - -**What They Do:** -- `WorkCompletionLearning.hook.ts` - Reads PRD.md frontmatter for work metadata and ISC section for criteria status, captures learning to `MEMORY/LEARNING/` for significant work sessions -- `SessionCleanup.hook.ts` - Marks PRD.md frontmatter status→COMPLETED and sets completed_at timestamp, clears session state, resets tab, cleans session names -- `RelationshipMemory.hook.ts` - Captures relationship context (observations, behaviors) to `MEMORY/RELATIONSHIP/` -- `UpdateCounts.hook.ts` - Updates system counts (skills, hooks, signals, workflows, files) displayed in the startup banner -- `IntegrityCheck.hook.ts` - Runs DocCrossRefIntegrity and SystemIntegrity checks at session end - ---- - -### 3. **UserPromptSubmit** -**When:** User submits a new prompt to Claude -**Use Cases:** -- Update UI indicators -- Pre-process user input -- Capture prompts for analysis -- Detect ratings and sentiment - -**Current Hooks:** -```json -{ - "UserPromptSubmit": [ - { - "hooks": [ - { - "type": "command", - "command": "${PAI_DIR}/hooks/RatingCapture.hook.ts" - }, - { - "type": "command", - "command": "${PAI_DIR}/hooks/UpdateTabTitle.hook.ts" - }, - { - "type": "command", - "command": "${PAI_DIR}/hooks/SessionAutoName.hook.ts" - } - ] - } - ] -} -``` - -**What They Do:** - -**RatingCapture.hook.ts** - Unified Rating Detection -- Handles both explicit ratings ("7", "8 - good work") and implicit sentiment analysis -- Explicit path: Pattern match first (no inference needed), writes to `ratings.jsonl` -- Implicit path: Haiku inference for sentiment if no explicit match -- Low ratings (<6) auto-capture as learning opportunities -- Writes to `~/.opencode/MEMORY/SIGNALS/ratings.jsonl` -- Uses shared libraries: `hooks/lib/learning-utils.ts`, `hooks/lib/time.ts` -- **Inference:** `import { inference } from '../PAI/Tools/Inference'` → `inference({ level: 'fast', expectJson: true })` - -**UpdateTabTitle.hook.ts** - Tab Title + Working State -- Updates Kitty terminal tab title with task summary + `…` suffix -- Sets tab to **orange background** (working state) -- Announces via voice server with context-appropriate gerund -- See `TERMINALTABS.md` for full state system documentation -- **Inference:** `import { inference } from '../PAI/Tools/Inference'` → `inference({ level: 'fast' })` - -**SessionAutoName.hook.ts** - Automatic Session Naming -- Infers a short descriptive name for the session from the first substantive prompt -- Updates `MEMORY/STATE/session-names.json` with the session ID → name mapping -- Used by the startup banner and session management tools -- **Inference:** `import { inference } from '../PAI/Tools/Inference'` → `inference({ level: 'fast' })` - ---- - -### 4. **Stop** -**When:** Main agent ({DAIDENTITY.NAME}) completes a response -**Use Cases:** -- Voice notifications for task completion -- Capture work summaries and learnings -- **Update terminal tab with final state** (color + suffix based on outcome) - -**Current Hooks:** -```json -{ - "Stop": [ - { - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/LastResponseCache.hook.ts" }, - { "type": "command", "command": "${PAI_DIR}/hooks/ResponseTabReset.hook.ts" }, - { "type": "command", "command": "${PAI_DIR}/hooks/VoiceCompletion.hook.ts" }, - { "type": "command", "command": "${PAI_DIR}/hooks/DocIntegrity.hook.ts" }, - { "type": "command", "command": "${PAI_DIR}/hooks/AlgorithmTab.hook.ts" } - ] - } - ] -} -``` - -**What They Do:** - -Each Stop hook is a self-contained `.hook.ts` file that reads stdin via shared `hooks/lib/hook-io.ts`, calls its handler, and exits. Handlers in `hooks/handlers/` are unchanged — each hook is a thin wrapper. - -**`LastResponseCache.hook.ts`** — Cache last response for RatingCapture bridge -- Writes `last_assistant_message` (or transcript fallback) to `MEMORY/STATE/last-response.txt` -- RatingCapture reads this on the next UserPromptSubmit to access the previous response - -**`ResponseTabReset.hook.ts`** — Reset Kitty tab title/color after response -- Calls `handlers/TabState.ts` to set completed state -- Converts working gerund title to past tense - -**`VoiceCompletion.hook.ts`** — Send 🗣️ voice line to TTS server -- Calls `handlers/VoiceNotification.ts` for voice delivery -- Voice gate: only main sessions (checks `kitty-sessions/{sessionId}.json`) -- Subagents have no kitty-sessions file → voice blocked - -**`AlgorithmTab.hook.ts`** — Show Algorithm phase + progress in Kitty tab title -- Reads `work.json`, finds most recently updated active session, sets tab title - -**`DocIntegrity.hook.ts`** — Cross-reference + semantic drift checks -- Calls `handlers/DocCrossRefIntegrity.ts` — deterministic + inference-powered doc updates -- Self-gating: returns instantly when no system files were modified - -**Tab State System:** See `TERMINALTABS.md` for complete documentation - ---- - -### 5. **PreToolUse** -**When:** Before Claude executes any tool -**Use Cases:** -- Voice curl gating (prevent background agents from speaking) -- Security validation across file operations (Bash, Edit, Write, Read) -- Tab state updates on questions -- Agent execution guardrails -- Skill invocation validation - -**Current Hooks:** -```json -{ - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } - ] - }, - { - "matcher": "Edit", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } - ] - }, - { - "matcher": "Write", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } - ] - }, - { - "matcher": "Read", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" } - ] - }, - { - "matcher": "AskUserQuestion", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/SetQuestionTab.hook.ts" } - ] - }, - { - "matcher": "Task", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/AgentExecutionGuard.hook.ts" } - ] - }, - { - "matcher": "Skill", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/SkillGuard.hook.ts" } - ] - } - ] -} -``` - -**What They Do:** -- `SecurityValidator.hook.ts` - Validates operations against security patterns. Runs on **4 matchers**: Bash (dangerous commands), Edit (sensitive file protection), Write (sensitive file protection), Read (sensitive path access) -- `SetQuestionTab.hook.ts` - Updates tab state to "awaiting input" when AskUserQuestion is invoked -- `AgentExecutionGuard.hook.ts` - Validates agent spawning (Task tool) against execution policies -- `SkillGuard.hook.ts` - Prevents false skill invocations (e.g., blocks keybindings-help unless explicitly requested) - ---- - -### 6. **PostToolUse** -**When:** After Claude executes any tool -**Status:** Active - Algorithm state tracking - -**Current Hooks:** -```json -{ - "PostToolUse": [ - { - "matcher": "AskUserQuestion", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/QuestionAnswered.hook.ts" } - ] - }, - { - "matcher": "Write", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/PRDSync.hook.ts" } - ] - }, - { - "matcher": "Edit", - "hooks": [ - { "type": "command", "command": "${PAI_DIR}/hooks/PRDSync.hook.ts" } - ] - } - ] -} -``` - -**What They Do:** - -**QuestionAnswered.hook.ts** - Post-Question Processing -- Fires after AskUserQuestion completes (user has answered) -- Captures the question and answer for session context -- Used for analytics and learning from user preferences - -**PRDSync.hook.ts** - PRD Frontmatter → work.json Sync -- Fires after Write/Edit to PRD files in `MEMORY/WORK/` -- Syncs PRD frontmatter (status, title, effort) to `MEMORY/STATE/work.json` -- Keeps work registry in sync without manual updates -- Non-blocking, fire-and-forget - ---- - -### 7. **PreCompact** -**When:** Before Claude compacts context (long conversations) -**Status:** Not currently configured - -**Potential Use Cases:** -- Preserve important context before compaction -- Log compaction events - ---- - -## Configuration - -### Location -**File:** `~/.opencode/settings.json` -**Section:** `"hooks": { ... }` - -### Environment Variables -Hooks have access to all environment variables from `~/.opencode/settings.json` `"env"` section: - -```json -{ - "env": { - "PAI_DIR": "$HOME/.opencode", - "CLAUDE_CODE_MAX_OUTPUT_TOKENS": "64000" - } -} -``` - -**Key Variables:** -- `PAI_DIR` - PAI installation directory (typically `~/.claude`) -- Hook scripts reference `${PAI_DIR}` in command paths - -### Identity Configuration (Central to Install Wizard) - -**settings.json is the single source of truth for all daidentity/configuration.** - -```json -{ - "daidentity": { - "name": "PAI", - "fullName": "Personal AI", - "displayName": "PAI", - "color": "#3B82F6", - "voiceId": "{YourElevenLabsVoiceId}" - }, - "principal": { - "name": "{YourName}", - "pronunciation": "{YourName}", - "timezone": "America/Los_Angeles" - } -} -``` - -**Using the Identity Module:** -```typescript -import { getIdentity, getPrincipal, getDAName, getPrincipalName, getVoiceId } from './lib/identity'; - -// Get full identity objects -const identity = getIdentity(); // { name, fullName, displayName, voiceId, color } -const principal = getPrincipal(); // { name, pronunciation, timezone } - -// Convenience functions -const DA_NAME = getDAName(); // "PAI" -const USER_NAME = getPrincipalName(); // "{YourName}" -const VOICE_ID = getVoiceId(); // from settings.json daidentity.voiceId -``` - -**Why settings.json?** -- Programmatic access via `JSON.parse()` - no regex parsing markdown -- Central to the PAI install wizard -- Single source of truth for all configuration -- Tool-friendly: easy to read/write from any language - -### Hook Configuration Structure - -```json -{ - "hooks": { - "HookEventName": [ - { - "matcher": "pattern", // Optional: filter which tools/events trigger hook - "hooks": [ - { - "type": "command", - "command": "${PAI_DIR}/hooks/my-hook.ts --arg value" - } - ] - } - ] - } -} -``` - -**Fields:** -- `HookEventName` - One of: SessionStart, SessionEnd, UserPromptSubmit, Stop, PreToolUse, PostToolUse, PreCompact -- `matcher` - Pattern to match (use `"*"` for all tools, or specific tool names) -- `type` - Always `"command"` (executes external script) -- `command` - Path to executable hook script (TypeScript/Python/Bash) - -### Hook Input (stdin) -All hooks receive JSON data on stdin: - -```typescript -{ - session_id: string; // Unique session identifier - transcript_path: string; // Path to JSONL transcript - hook_event_name: string; // Event that triggered hook - prompt?: string; // User prompt (UserPromptSubmit only) - tool_name?: string; // Tool name (PreToolUse/PostToolUse) - tool_input?: any; // Tool parameters (PreToolUse) - tool_output?: any; // Tool result (PostToolUse) - // ... event-specific fields -} -``` - ---- - -## Common Patterns - -### 1. Voice Notifications - -**Pattern:** Extract completion message → Send to voice server - -```typescript -// handlers/VoiceNotification.ts pattern -import { getIdentity } from './lib/identity'; - -const identity = getIdentity(); -const completionMessage = extractCompletionMessage(lastMessage); - -const payload = { - title: identity.name, - message: completionMessage, - voice_enabled: true, - voice_id: identity.voiceId // From settings.json -}; - -await fetch('http://localhost:8888/notify', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload) -}); -``` - -**Agent-Specific Voices:** -Configure voice IDs via `settings.json` daidentity section or environment variables. -Each agent can have a unique ElevenLabs voice configured. See the Agents skill for voice registry. - ---- - -### 2. History Capture (UOCS Pattern) - -**Pattern:** Parse structured response → Save to appropriate history directory - -**File Naming Convention:** -``` -YYYY-MM-DD-HHMMSS_TYPE_description.md -``` - -**Types:** -- `WORK` - General task completions -- `LEARNING` - Problem-solving learnings -- `SESSION` - Session summaries -- `RESEARCH` - Research findings (from agents) -- `FEATURE` - Feature implementations (from agents) -- `DECISION` - Architectural decisions (from agents) - -**Example pattern (from WorkCompletionLearning.hook.ts):** -```typescript -import { getLearningCategory, isLearningCapture } from './lib/learning-utils'; -import { getPSTTimestamp, getYearMonth } from './lib/time'; - -const structured = extractStructuredSections(lastMessage); -const isLearning = isLearningCapture(text, structured.summary, structured.analysis); - -// If learning content detected, capture to LEARNING/ -if (isLearning) { - const category = getLearningCategory(text); // 'SYSTEM' or 'ALGORITHM' - const targetDir = join(baseDir, 'MEMORY', 'LEARNING', category, getYearMonth()); - const filename = generateFilename(description, 'LEARNING'); - writeFileSync(join(targetDir, filename), content); -} -``` - -**Structured Sections Parsed:** -- `📋 SUMMARY:` - Brief overview -- `🔍 ANALYSIS:` - Key findings -- `⚡ ACTIONS:` - Steps taken -- `✅ RESULTS:` - Outcomes -- `📊 STATUS:` - Current state -- `➡️ NEXT:` - Follow-up actions -- `🎯 COMPLETED:` - **Voice notification line** - ---- - -### 3. Agent Type Detection - -**Pattern:** Identify which agent is executing → Route appropriately - -```typescript -// Agent detection pattern -let agentName = getAgentForSession(sessionId); - -// Detect from Task tool -if (hookData.tool_name === 'Task' && hookData.tool_input?.subagent_type) { - agentName = hookData.tool_input.subagent_type; - setAgentForSession(sessionId, agentName); -} - -// Detect from CLAUDE_CODE_AGENT env variable -else if (process.env.CLAUDE_CODE_AGENT) { - agentName = process.env.CLAUDE_CODE_AGENT; -} - -// Detect from path (subagents run in /agents/name/) -else if (hookData.cwd && hookData.cwd.includes('/agents/')) { - const agentMatch = hookData.cwd.match(/\/agents\/([^\/]+)/); - if (agentMatch) agentName = agentMatch[1]; -} -``` - -**Session Mapping:** `~/.opencode/MEMORY/STATE/agent-sessions.json` -```json -{ - "session-id-abc123": "engineer", - "session-id-def456": "researcher" -} -``` - ---- - -### 4. Tab Title + Color State Architecture - -**Pattern:** Visual state feedback through tab colors and title suffixes - -**State Flow:** - -| Event | Hook | Tab Title | Inactive Color | State | -|-------|------|-----------|----------------|-------| -| UserPromptSubmit | `UpdateTabTitle.hook.ts` | `⚙️ Summary…` | Orange `#B35A00` | Working | -| Inference | `UpdateTabTitle.hook.ts` | `🧠 Analyzing…` | Orange `#B35A00` | Inference | -| Stop (success) | `handlers/TabState.ts` | `Summary` | Green `#022800` | Completed | -| Stop (question) | `handlers/TabState.ts` | `Summary?` | Teal `#0D4F4F` | Awaiting Input | -| Stop (error) | `handlers/TabState.ts` | `Summary!` | Orange `#B35A00` | Error | - -**Active Tab:** Always Dark Blue `#002B80` (state colors only affect inactive tabs) - -**Why This Design:** -- **Instant visual feedback** - See state at a glance without reading -- **Color-coded priority** - Teal tabs need attention, green tabs are done -- **Suffix as state indicator** - Works even in narrow tab bars -- **Haiku only on user input** - One AI call per prompt (not per tool) - -**State Detection (in Stop hook):** -1. Check transcript for `AskUserQuestion` tool → `awaitingInput` -2. Check `📊 STATUS:` for error patterns → `error` -3. Default → `completed` - -**Text Colors:** -- Active tab: White `#FFFFFF` (always) -- Inactive tab: Gray `#A0A0A0` (always) - -**Active Tab Background:** Dark Blue `#002B80` (always - state colors only affect inactive tabs) - -**Tab Icons:** -- 🧠 Brain - AI inference in progress (Haiku/Sonnet thinking) -- ⚙️ Gear - Processing/working state - -**Full Documentation:** See `~/.opencode/PAI/TERMINALTABS.md` - ---- - -### 5. Async Non-Blocking Execution - -**Pattern:** Hook executes quickly → Launch background processes for slow operations - -```typescript -// update-tab-titles.ts pattern -// Set immediate tab title (fast) -execSync(`printf '\\033]0;${titleWithEmoji}\\007' >&2`); - -// Launch background process for Haiku summary (slow) -Bun.spawn(['bun', `${paiDir}/hooks/UpdateTabTitle.ts`, prompt], { - stdout: 'ignore', - stderr: 'ignore', - stdin: 'ignore' -}); - -process.exit(0); // Exit immediately -``` - -**Key Principle:** Hooks must never block Claude Code. Always exit quickly, use background processes for slow work. - ---- - -### 6. Graceful Failure - -**Pattern:** Wrap everything in try/catch → Log errors → Exit successfully - -```typescript -async function main() { - try { - // Hook logic here - } catch (error) { - // Log but don't fail - console.error('Hook error:', error); - } - - process.exit(0); // Always exit 0 -} -``` - -**Why:** If hooks crash, Claude Code may freeze. Always exit cleanly. - ---- - -## Creating Custom Hooks - -### Step 1: Choose Hook Event -Decide which event should trigger your hook (SessionStart, Stop, PostToolUse, etc.) - -### Step 2: Create Hook Script -**Location:** `~/.opencode/hooks/my-custom-hook.ts` - -**Template:** -```typescript -#!/usr/bin/env bun - -interface HookInput { - session_id: string; - transcript_path: string; - hook_event_name: string; - // ... event-specific fields -} - -async function main() { - try { - // Read stdin - const input = await Bun.stdin.text(); - const data: HookInput = JSON.parse(input); - - // Your hook logic here - console.log(`Hook triggered: ${data.hook_event_name}`); - - // Example: Read transcript - const fs = require('fs'); - const transcript = fs.readFileSync(data.transcript_path, 'utf-8'); - - // Do something with the data - - } catch (error) { - // Log but don't fail - console.error('Hook error:', error); - } - - process.exit(0); // Always exit 0 -} - -main(); -``` - -### Step 3: Make Executable -```bash -chmod +x ~/.opencode/hooks/my-custom-hook.ts -``` - -### Step 4: Add to settings.json -```json -{ - "hooks": { - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "${PAI_DIR}/hooks/my-custom-hook.ts" - } - ] - } - ] - } -} -``` - -### Step 5: Test -```bash -# Test hook directly -echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ~/.opencode/hooks/my-custom-hook.ts -``` - -### Step 6: Restart Claude Code -Hooks are loaded at startup. Restart to apply changes. - ---- - -## Hook Development Best Practices - -### 1. **Fast Execution** -- Hooks should complete in < 500ms -- Use background processes for slow work (Haiku API calls, file processing) -- Exit immediately after launching background work - -### 2. **Graceful Failure** -- Always wrap in try/catch -- Log errors to stderr (available in hook debug logs) -- Always `process.exit(0)` - never throw or exit(1) - -### 3. **Non-Blocking** -- Never wait for external services (unless they respond quickly) -- Use `.catch(() => {})` for async operations -- Fail silently if optional services are offline - -### 4. **Stdin Reading** -- Use timeout when reading stdin (Claude Code may not send data immediately) -- Handle empty/invalid input gracefully - -```typescript -const decoder = new TextDecoder(); -const reader = Bun.stdin.stream().getReader(); - -const timeoutPromise = new Promise((resolve) => { - setTimeout(() => resolve(), 500); // 500ms timeout -}); - -await Promise.race([readPromise, timeoutPromise]); -``` - -### 5. **File I/O** -- Check `existsSync()` before reading files -- Create directories with `{ recursive: true }` -- Use PST timestamps for consistency - -### 6. **Environment Access** -- All `settings.json` env vars available via `process.env` -- Use `${PAI_DIR}` in settings.json for portability -- Access in code via `process.env.PAI_DIR` - -### 7. **Logging** -- Log useful debug info to stderr for troubleshooting -- Include relevant metadata (session_id, tool_name, etc.) -- Never log sensitive data (API keys, user content) - ---- - -## Troubleshooting - -### Hook Not Running - -**Check:** -1. Is hook script executable? `chmod +x ~/.opencode/hooks/my-hook.ts` -2. Is path correct in settings.json? Use `${PAI_DIR}/hooks/...` -3. Is settings.json valid JSON? `jq . ~/.opencode/settings.json` -4. Did you restart Claude Code after editing settings.json? - -**Debug:** -```bash -# Test hook directly -echo '{"session_id":"test","transcript_path":"/tmp/test.jsonl","hook_event_name":"Stop"}' | bun ~/.opencode/hooks/my-hook.ts - -# Check hook logs (stderr output) -tail -f ~/.opencode/hooks/debug.log # If you add logging -``` - ---- - -### Hook Hangs/Freezes Claude Code - -**Cause:** Hook not exiting (infinite loop, waiting for input, blocking operation) - -**Fix:** -1. Add timeouts to all blocking operations -2. Ensure `process.exit(0)` is always reached -3. Use background processes for long operations -4. Check stdin reading has timeout - -**Prevention:** -```typescript -// Always use timeout -setTimeout(() => { - console.error('Hook timeout - exiting'); - process.exit(0); -}, 5000); // 5 second max -``` - ---- - -### Voice Notifications Not Working - -**Check:** -1. Is voice server running? `curl http://localhost:8888/health` -2. Is voice_id correct? See `PAI/SKILL.md` for mappings -3. Is message format correct? `{"message":"...", "voice_id":"...", "title":"..."}` -4. Is ElevenLabs API key in `${PAI_DIR}/.env`? - -**Debug:** -```bash -# Test voice server directly -curl -X POST http://localhost:8888/notify \ - -H "Content-Type: application/json" \ - -d '{"message":"Test message","voice_id":"[YOUR_VOICE_ID]","title":"Test"}' -``` - -**Common Issues:** -- Wrong voice_id → Silent failure (invalid ID) -- Voice server offline → Hook continues (graceful failure) -- No `🎯 COMPLETED:` line → No voice notification extracted - ---- - -### Work Not Capturing - -**Check:** -1. Does `~/.opencode/MEMORY/` directory exist? -2. Does current-work file exist? Check `~/.opencode/MEMORY/STATE/current-work.json` -3. Is hook actually running? Check `~/.opencode/MEMORY/RAW/` for events -4. File permissions? `ls -la ~/.opencode/MEMORY/WORK/` - -**Debug:** -```bash -# Check current work -cat ~/.opencode/MEMORY/STATE/current-work.json - -# Check recent work directories -ls -lt ~/.opencode/MEMORY/WORK/ | head -10 -ls -lt ~/.opencode/MEMORY/LEARNING/$(date +%Y-%m)/ | head -10 - -# Check raw events -tail ~/.opencode/MEMORY/RAW/$(date +%Y-%m)/$(date +%Y-%m-%d)_all-events.jsonl -``` - -**Common Issues:** -- Missing current-work.json → Work not being tracked for this session -- Work not updating → capture handler not finding current work -- Learning detection too strict → Adjust `isLearningCapture()` logic - ---- - -### Stop Event Not Firing (RESOLVED) - -**Original Issue:** Stop events were not firing consistently in earlier Claude Code versions, causing voice notifications and work capture to fail silently. - -**Resolution:** Fixed in Claude Code updates. The Stop hooks now fires reliably. The unified orchestrator pattern (`Stop hooks.hook.ts` delegating to `handlers/`) was implemented in part to work around this — and remains the production architecture. - -**Status:** RESOLVED — Stop events now fire reliably. Stop hooks handles all post-response work. - ---- - -### Agent Detection Failing - -**Check:** -1. Is `~/.opencode/MEMORY/STATE/agent-sessions.json` writable? -2. Is `[AGENT:type]` tag in `🎯 COMPLETED:` line? -3. Is agent running from correct directory? (`/agents/name/`) - -**Debug:** -```bash -# Check session mappings -cat ~/.opencode/MEMORY/STATE/agent-sessions.json | jq . - -# Check subagent-stop debug log -tail -f ~/.opencode/hooks/subagent-stop-debug.log -``` - -**Fix:** -- Ensure agents include `[AGENT:type]` in completion line -- Verify Task tool passes `subagent_type` parameter -- Check cwd includes `/agents/` in path - ---- - -### Transcript Type Mismatch (Fixed 2026-01-11) - -**Symptom:** Context reading functions return empty results even though transcript has data - -**Root Cause:** Claude Code transcripts use `type: "user"` but hooks were checking for `type: "human"`. - -**Affected Hooks:** -- `UpdateTabTitle.hook.ts` - Couldn't read user messages for context -- `RatingCapture.hook.ts` - Same issue - -**Fix Applied:** -1. Changed `entry.type === 'human'` → `entry.type === 'user'` -2. Improved content extraction to skip `tool_result` blocks and only capture actual text - -**Verification:** -```bash -# Check transcript type field -grep '"type":"user"' ~/.opencode/projects/-Users-username--claude/*.jsonl | head -1 | jq '.type' -# Should output: "user" (not "human") -``` - -**Prevention:** When parsing transcripts, always verify the actual JSON structure first. - ---- - -### Context Loading Issues (SessionStart) - -**Check:** -1. Does `~/.opencode/PAI/SKILL.md` exist? -2. Is `LoadContext.hook.ts` executable? -3. Is `PAI_DIR` env variable set correctly? - -**Debug:** -```bash -# Test context loading directly -bun ~/.opencode/hooks/LoadContext.hook.ts - -# Should output with SKILL.md content -``` - -**Common Issues:** -- Subagent sessions loading main context → Fixed (subagent detection in hook) -- File not found → Check `PAI_DIR` environment variable -- Permission denied → `chmod +x ~/.opencode/hooks/LoadContext.hook.ts` - ---- - -## Advanced Topics - -### Multi-Hook Execution Order - -Hooks in same event execute **sequentially** in order defined in settings.json: - -```json -{ - "Stop": [ - { - "hooks": [ - { "command": "${PAI_DIR}/hooks/Stop hooks.hook.ts" } // Single orchestrator - ] - } - ] -} -``` - -**Note:** If first hook hangs, second won't run. Keep hooks fast! - ---- - -### Matcher Patterns - -`"matcher"` field filters which events trigger hook: - -```json -{ - "PostToolUse": [ - { - "matcher": "Bash", // Only Bash tool executions - "hooks": [...] - }, - { - "matcher": "*", // All tool executions - "hooks": [...] - } - ] -} -``` - -**Patterns:** -- `"*"` - All events -- `"Bash"` - Specific tool name -- `""` - Empty (all events, same as `*`) - ---- - -### Hook Data Payloads by Event Type - -**SessionStart:** -```typescript -{ - session_id: string; - transcript_path: string; - hook_event_name: "SessionStart"; - cwd: string; -} -``` - -**UserPromptSubmit:** -```typescript -{ - session_id: string; - transcript_path: string; - hook_event_name: "UserPromptSubmit"; - prompt: string; // The user's prompt text -} -``` - -**PreToolUse:** -```typescript -{ - session_id: string; - transcript_path: string; - hook_event_name: "PreToolUse"; - tool_name: string; - tool_input: any; // Tool parameters -} -``` - -**PostToolUse:** -```typescript -{ - session_id: string; - transcript_path: string; - hook_event_name: "PostToolUse"; - tool_name: string; - tool_input: any; - tool_output: any; // Tool result - error?: string; // If tool failed -} -``` - -**Stop:** -```typescript -{ - session_id: string; - transcript_path: string; - hook_event_name: "Stop"; -} -``` - -**SessionEnd:** -```typescript -{ - conversation_id: string; // Note: different field name - timestamp: string; -} -``` - ---- - -## Related Documentation - -- **Voice System:** `~/.opencode/VoiceServer/SKILL.md` -- **Agent System:** `~/.opencode/skills/Agents/SKILL.md` -- **History/Memory:** `~/.opencode/PAI/MEMORYSYSTEM.md` - ---- - -## Quick Reference Card - -``` -HOOK LIFECYCLE: -1. Event occurs (SessionStart, Stop, etc.) -2. Claude Code writes hook data to stdin -3. Hook script executes -4. Hook reads stdin (with timeout) -5. Hook performs actions (voice, capture, etc.) -6. Hook exits 0 (always succeeds) -7. Claude Code continues - -HOOKS BY EVENT (22 hooks total): - -SESSION START (2 hooks): - KittyEnvPersist.hook.ts Persist Kitty env vars + tab reset - LoadContext.hook.ts Dynamic context injection (relationship, learning, work) - -SESSION END (5 hooks): - WorkCompletionLearning.hook.ts Work/learning capture to MEMORY/ - SessionCleanup.hook.ts Mark WORK dir complete, clear state, reset tab - RelationshipMemory.hook.ts Relationship context to MEMORY/RELATIONSHIP/ - UpdateCounts.hook.ts Refresh system counts (skills, hooks, signals) - IntegrityCheck.hook.ts System integrity checks - -USER PROMPT SUBMIT (3 hooks): - RatingCapture.hook.ts Unified rating capture (explicit + implicit) - UpdateTabTitle.hook.ts Tab title + working state (orange) - SessionAutoName.hook.ts Auto-name session from first prompt - -STOP (5 hooks): - LastResponseCache.hook.ts Cache response for RatingCapture bridge - ResponseTabReset.hook.ts Tab title/color reset after response - VoiceCompletion.hook.ts Voice TTS (main sessions only) - DocIntegrity.hook.ts Cross-ref + semantic drift checks - AlgorithmTab.hook.ts Algorithm phase + progress in tab - -PRE TOOL USE (4 hooks): - SecurityValidator.hook.ts Security validation [Bash, Edit, Write, Read] - SetQuestionTab.hook.ts Tab state on question [AskUserQuestion] - AgentExecutionGuard.hook.ts Agent spawn guardrails [Task] - SkillGuard.hook.ts Skill invocation validation [Skill] - -POST TOOL USE (2 hooks): - QuestionAnswered.hook.ts Post-question tab reset [AskUserQuestion] - PRDSync.hook.ts PRD → work.json sync [Write, Edit] - -KEY FILES: -~/.opencode/settings.json Hook configuration -~/.opencode/hooks/ Hook scripts (22 files) -~/.opencode/hooks/handlers/ Handler modules (6 files) -~/.opencode/hooks/lib/ Shared libraries (13 files) -~/.opencode/hooks/lib/learning-utils.ts Learning categorization -~/.opencode/hooks/lib/time.ts PST timestamp utilities -~/.opencode/hooks/lib/event-types.ts Typed event definitions (22 interfaces) -~/.opencode/hooks/lib/event-emitter.ts appendEvent() → events.jsonl -~/.opencode/MEMORY/WORK/ Work tracking -~/.opencode/MEMORY/LEARNING/ Learning captures -~/.opencode/MEMORY/STATE/ Runtime state -~/.opencode/MEMORY/STATE/events.jsonl Unified event log (append-only) - -INFERENCE TOOL (for hooks needing AI): -Path: ~/.opencode/PAI/Tools/Inference.ts -Import: import { inference } from '../PAI/Tools/Inference' -Levels: fast (haiku/15s) | standard (sonnet/30s) | smart (opus/90s) - -TAB STATE SYSTEM: -Inference: 🧠… Orange #B35A00 (AI thinking) -Working: ⚙️… Orange #B35A00 (processing) -Completed: Green #022800 (task done) -Awaiting: ? Teal #0D4F4F (needs input) -Error: ! Orange #B35A00 (problem detected) -Active Tab: Always Dark Blue #002B80 (state colors = inactive only) - -VOICE SERVER: -URL: http://localhost:8888/notify -Payload: {"message":"...", "voice_id":"...", "title":"..."} -Configure voice IDs in individual agent files (`agents/*.md` persona frontmatter) - -``` - ---- - -## Shared Libraries - -The hook system uses shared TypeScript libraries to eliminate code duplication: - -### `hooks/lib/learning-utils.ts` -Shared learning categorization logic. - -```typescript -import { getLearningCategory, isLearningCapture } from './lib/learning-utils'; - -// Categorize learning as SYSTEM (tooling/infra) or ALGORITHM (task execution) -const category = getLearningCategory(content, comment); -// Returns: 'SYSTEM' | 'ALGORITHM' - -// Check if response contains learning indicators -const isLearning = isLearningCapture(text, summary, analysis); -// Returns: boolean (true if 2+ learning indicators found) -``` - -**Used by:** RatingCapture, WorkCompletionLearning - -### `hooks/lib/time.ts` -Shared PST timestamp utilities. - -```typescript -import { - getPSTTimestamp, // "2026-01-10 20:30:00 PST" - getPSTDate, // "2026-01-10" - getYearMonth, // "2026-01" - getISOTimestamp, // ISO8601 with offset - getFilenameTimestamp, // "2026-01-10-203000" - getPSTComponents // { year, month, day, hours, minutes, seconds } -} from './lib/time'; -``` - -**Used by:** RatingCapture, WorkCompletionLearning, SessionSummary - -### `hooks/lib/identity.ts` -Identity and principal configuration from settings.json. - -```typescript -import { getIdentity, getPrincipal, getDAName, getPrincipalName, getVoiceId } from './lib/identity'; - -const identity = getIdentity(); // { name, fullName, displayName, voiceId, color } -const principal = getPrincipal(); // { name, pronunciation, timezone } -``` - -**Used by:** handlers/VoiceNotification.ts, RatingCapture, handlers/TabState.ts - -### `PAI/Tools/Inference.ts` -Unified AI inference with three run levels. - -```typescript -import { inference } from '../PAI/Tools/Inference'; - -// Fast (Haiku) - quick tasks, 15s timeout -const result = await inference({ - systemPrompt: 'Summarize in 3 words', - userPrompt: text, - level: 'fast', -}); - -// Standard (Sonnet) - balanced reasoning, 30s timeout -const result = await inference({ - systemPrompt: 'Analyze sentiment', - userPrompt: text, - level: 'standard', - expectJson: true, -}); - -// Smart (Opus) - deep reasoning, 90s timeout -const result = await inference({ - systemPrompt: 'Strategic analysis', - userPrompt: text, - level: 'smart', -}); - -// Result shape -interface InferenceResult { - success: boolean; - output: string; - parsed?: unknown; // if expectJson: true - error?: string; - latencyMs: number; - level: 'fast' | 'standard' | 'smart'; -} -``` - -**Used by:** RatingCapture, UpdateTabTitle, SessionAutoName - ---- - -## Unified Event System - -Alongside existing filesystem state writes (algorithm-state JSON, PRDs, session-names.json, etc.), hooks can emit structured events to a single append-only JSONL log. This provides a unified observability layer without replacing any existing state management. - -### Components - -| File | Purpose | -|------|---------| -| `${PAI_DIR}/hooks/lib/event-types.ts` | TypeScript discriminated union of all PAI event types (22 interfaces covering algorithm, work, session, rating, learning, voice, PRD, doc, build, system, tab, hook error, and custom events) | -| `${PAI_DIR}/hooks/lib/event-emitter.ts` | `appendEvent()` utility that writes typed events to `${PAI_DIR}/MEMORY/STATE/events.jsonl` | - -### Usage in Hooks - -Hooks call `appendEvent()` as a secondary write **alongside** their existing state writes. The emitter is synchronous, fire-and-forget, and silently swallows errors so it never blocks or crashes a hook. - -```typescript -import { appendEvent } from './lib/event-emitter'; - -// Inside an existing hook, AFTER the normal state write: -appendEvent({ type: 'work.created', source: 'PRDSync', slug: 'my-task' }); -``` - -### Event Structure - -Every event has a common base shape plus type-specific fields: -- `timestamp` (ISO 8601) -- auto-injected by `appendEvent()` -- `session_id` -- auto-injected from `CLAUDE_SESSION_ID` env -- `source` -- the hook or handler name that emitted the event -- `type` -- dot-separated topic (e.g., `algorithm.phase`, `work.created`, `voice.sent`, `rating.captured`) - -Events use a dot-separated topic hierarchy for filtering. A `custom.*` escape hatch allows arbitrary extension without modifying the type system. - -### Event Type Categories - -| Category | Types | Emitting Hooks | -|----------|-------|----------------| -| `work.*` | created, completed | PRDSync, SessionCleanup | -| `session.*` | named, completed | SessionCleanup | -| `rating.*` | captured | RatingCapture | -| `learning.*` | captured | WorkCompletionLearning | -| `voice.*` | sent | VoiceNotification | -| `prd.*` | synced | PRDSync | -| `doc.*` | integrity | DocIntegrity | -| `build.*` | rebuild | BuildCLAUDE (SessionStart handler) | -| `system.*` | integrity | IntegrityCheck | -| `settings.*` | counts_updated | UpdateCounts | -| `tab.*` | updated | TabState, UpdateTabTitle | -| `hook.*` | error | Any hook (error reporting) | -| `custom.*` | user-defined | Extensibility escape hatch | - -### Consuming Events - -```bash -# Live tail (real-time monitoring) -tail -f ~/.opencode/MEMORY/STATE/events.jsonl | jq - -# Filter by type -tail -f ~/.opencode/MEMORY/STATE/events.jsonl | jq 'select(.type | startswith("algorithm."))' - -# Programmatic (Node/Bun fs.watch) -import { watch } from 'fs'; -import { getEventsPath } from './hooks/lib/event-emitter'; -watch(getEventsPath(), (eventType) => { /* read new lines */ }); -``` - -### Key Principles - -- **Additive only** -- events supplement existing state files, they never replace them -- **Append-only** -- `events.jsonl` is an immutable log, never rewritten or truncated by hooks -- **Graceful failure** -- write errors are swallowed; events are observability, not critical path -- **One file** -- all event types go to a single `events.jsonl` for simple tailing and watching - ---- - -**Last Updated:** 2026-02-25 -**Status:** Production - 15 hooks emitting 22 event types across 14 categories -**Maintainer:** PAI System diff --git a/.opencode/PAI/Tools/GenerateSkillIndex.ts b/.opencode/PAI/Tools/GenerateSkillIndex.ts index a8ffdc2d..5caaf904 100644 --- a/.opencode/PAI/Tools/GenerateSkillIndex.ts +++ b/.opencode/PAI/Tools/GenerateSkillIndex.ts @@ -360,7 +360,9 @@ async function main() { const avgMinimalTokens = 25; const currentTokens = index.totalSkills * avgFullTokens; const newTokens = (index.alwaysLoadedCount * avgFullTokens) + (index.deferredCount * avgMinimalTokens); - const savings = ((currentTokens - newTokens) / currentTokens * 100).toFixed(1); + const savings = currentTokens > 0 + ? ((currentTokens - newTokens) / currentTokens * 100).toFixed(1) + : "0.0"; console.log(`\n💰 Estimated token impact:`); console.log(` Current: ~${currentTokens.toLocaleString()} tokens`); diff --git a/.opencode/PAI/doc-dependencies.json b/.opencode/PAI/doc-dependencies.json index af21077c..3512d30f 100644 --- a/.opencode/PAI/doc-dependencies.json +++ b/.opencode/PAI/doc-dependencies.json @@ -23,9 +23,9 @@ "heading": "## Skill System Architecture", "dependents": ["SKILLSYSTEM.md"] }, - "hook-system": { - "heading": "## Hook System Architecture", - "dependents": ["THEHOOKSYSTEM.md"] + "plugin-system": { + "heading": "## Plugin System Architecture", + "dependents": ["THEPLUGINSYSTEM.md"] }, "memory-system": { "heading": "## Memory System Architecture", @@ -95,12 +95,6 @@ "upstream": "PAISYSTEMARCHITECTURE.md" }, - "THEHOOKSYSTEM.md": { - "description": "Hook system detailed documentation", - "tier": 2, - "upstream": "PAISYSTEMARCHITECTURE.md" - }, - "THEPLUGINSYSTEM.md": { "description": "Plugin system documentation (OpenCode adaptation of Hook system, see ADR-001)", "tier": 2, diff --git a/.opencode/PAISECURITYSYSTEM/ARCHITECTURE.md b/.opencode/PAISECURITYSYSTEM/ARCHITECTURE.md index 7d1d908d..4a9faa56 100755 --- a/.opencode/PAISECURITYSYSTEM/ARCHITECTURE.md +++ b/.opencode/PAISECURITYSYSTEM/ARCHITECTURE.md @@ -10,7 +10,7 @@ PAI uses a 4-layer defense-in-depth model: ``` Layer 1: settings.json permissions → Allow list for tools (fast, native) -Layer 2: SecurityValidator hook → patterns.yaml validation (blocking) +Layer 2: SecurityValidator plugin → patterns.yaml validation (blocking) Layer 3: Security Event Logging → All events to MEMORY/SECURITY/ (audit) Layer 4: Git version control → Rollback via git restore/checkout ``` @@ -51,13 +51,13 @@ The result: you get meaningful protection without the friction that drives peopl - MCP servers: `mcp__*` - Task delegation tools -### Blocked via Hook (hard block) +### Blocked via Plugin (hard block) Irreversible, catastrophic operations: - Filesystem destruction: `r.m -rf /`, `r.m -rf ~` - Disk operations: `disk.util erase*`, `d.d if=/dev/zero`, `mk.fs` - Repository exposure: `g.h repo delete`, `g.h repo edit --visibility public` -### Confirm via Hook (prompt first) +### Confirm via Plugin (prompt first) Dangerous but sometimes legitimate: - Git force operations: `git push --force`, `git reset --hard` - Cloud destructive: AWS/GCP/Terraform deletion commands @@ -74,11 +74,10 @@ Suspicious but allowed: ### Bash Patterns -| Level | Exit Code | Behavior | -|-------|-----------|----------| -| `blocked` | 2 | Hard block, operation prevented | -| `confirm` | 0 + JSON | User prompted for confirmation | -| `alert` | 0 | Logged but allowed | +| Level | Action | Behavior | +|-------|--------|----------| +| `blocked` | throw Error | Hard block, operation prevented | +| `alert` | log + return | Logged but allowed | ### Path Patterns @@ -112,14 +111,14 @@ ZERO TRUST: External websites, APIs, unknown documents --- -## Hook Execution Flow +## Plugin Execution Flow ``` User Action (Bash/Edit/Write/Read) ↓ - PreToolUse Hook Triggered + tool.execute.before Event Triggered ↓ -SecurityValidator.hook.ts Runs +SecurityValidator Plugin Runs (pai-unified.ts) ↓ Loads patterns.yaml (USER first, then root fallback) ↓ @@ -129,8 +128,7 @@ SecurityValidator.hook.ts Runs • Projects: special rules per project ↓ Decision: - ├─ block → exit(2), hard block - ├─ confirm → JSON {"decision":"ask"}, prompt user + ├─ block → throw Error, hard block ├─ alert → log, allow execution └─ allow → proceed normally ↓ @@ -197,8 +195,8 @@ git stash |------|---------| | `USER/PAISECURITYSYSTEM/patterns.yaml` | User's security rules (primary) | | `PAISECURITYSYSTEM/patterns.example.yaml` | Default template (fallback) | -| `hooks/SecurityValidator.hook.ts` | Enforcement logic | -| `settings.json` | Hook configuration | +| `plugins/pai-unified.ts` | Enforcement logic | +| `opencode.json` | Plugin configuration | | `MEMORY/SECURITY/YYYY/MM/security-*.jsonl` | Security event logs (one per event) | --- @@ -210,9 +208,9 @@ To customize security for your environment: 1. Copy `PAISECURITYSYSTEM/patterns.example.yaml` to `USER/PAISECURITYSYSTEM/patterns.yaml` 2. Edit patterns to match your needs 3. Add project-specific rules in the `projects` section -4. The hook automatically loads USER patterns when available +4. The plugin automatically loads USER patterns when available -See `PAISECURITYSYSTEM/HOOKS.md` for hook configuration details. +See `PAISECURITYSYSTEM/PLUGINS.md` for plugin configuration details. --- diff --git a/.opencode/PAISECURITYSYSTEM/HOOKS.md b/.opencode/PAISECURITYSYSTEM/HOOKS.md deleted file mode 100644 index 9eefe044..00000000 --- a/.opencode/PAISECURITYSYSTEM/HOOKS.md +++ /dev/null @@ -1,254 +0,0 @@ -# SecurityValidator Hook Documentation - -**How the security validation hook works** - ---- - -## Overview - -`SecurityValidator.hook.ts` is a PreToolUse hook that validates Bash commands and file operations against security patterns before execution. It prevents catastrophic operations while allowing normal development work. - ---- - -## Trigger - -- **Event:** PreToolUse -- **Matchers:** Bash, Edit, Write, Read - ---- - -## Input - -The hook receives JSON via stdin: - -```json -{ - "tool_name": "Bash", - "tool_input": { - "command": "rm -rf /some/path" - }, - "session_id": "abc-123-uuid" -} -``` - -For file operations: -```json -{ - "tool_name": "Write", - "tool_input": { - "file_path": "/path/to/file.txt", - "content": "..." - }, - "session_id": "abc-123-uuid" -} -``` - ---- - -## Output - -The hook communicates decisions via exit codes and stdout: - -| Exit Code | Stdout | Result | -|-----------|--------|--------| -| 0 | `{"continue": true}` | Allow operation | -| 0 | `{"decision": "ask", "message": "..."}` | Prompt user for confirmation | -| 2 | (any) | Hard block - operation prevented | - ---- - -## Pattern Loading - -The hook loads patterns in this order: - -1. **USER patterns** (primary): `USER/PAISECURITYSYSTEM/patterns.yaml` -2. **SYSTEM patterns** (fallback): `PAISECURITYSYSTEM/patterns.example.yaml` -3. **Fail-open**: If neither exists, allow all operations - -This cascading approach ensures: -- Users can customize their own security rules -- New installations work with sensible defaults -- Missing configuration doesn't block work - ---- - -## Pattern Matching - -### Bash Commands - -```yaml -bash: - blocked: # Hard block (exit 2) - - pattern: "rm -rf /" - reason: "Filesystem destruction" - - confirm: # User prompt (exit 0 + JSON) - - pattern: "git push --force" - reason: "Force push can lose commits" - - alert: # Log only - - pattern: "curl.*\\|.*sh" - reason: "Piping curl to shell" -``` - -Patterns are evaluated as regular expressions (case-insensitive). - -### Path Protection - -```yaml -paths: - zeroAccess: # Complete denial - - "~/.ssh/id_*" - - readOnly: # Can read, not write - - "/etc/**" - - confirmWrite: # Writing needs confirmation - - "**/.env" - - noDelete: # Cannot delete - - ".git/**" -``` - -Path patterns use glob syntax: -- `*` matches any characters except `/` -- `**` matches any characters including `/` -- `~` expands to home directory - ---- - -## Execution Flow - -``` -1. Parse stdin JSON -2. Load patterns (USER → SYSTEM → empty) -3. Determine tool type (Bash vs file operation) -4. For Bash: Check command against bash patterns -5. For files: Check path against path patterns -6. Log security event (all decisions) -7. Return decision (exit code + JSON) -``` - ---- - -## Security Event Logging - -All decisions are logged as individual files: `MEMORY/SECURITY/YYYY/MM/security-{summary}-{timestamp}.jsonl` - -Each event gets a descriptive filename (e.g., `security-block-filesystem-destruction-20260114-143052.jsonl`). - -```json -{ - "timestamp": "2026-01-14T12:00:00.000Z", - "session_id": "abc-123", - "event_type": "block", - "tool": "Bash", - "category": "bash_command", - "target": "rm -rf /", - "pattern_matched": "rm -rf /", - "reason": "Filesystem destruction", - "action_taken": "blocked" -} -``` - ---- - -## Configuration - -Enable the hook in `settings.json`: - -```json -{ - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [{ - "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" - }] - }, - { - "matcher": "Edit", - "hooks": [{ - "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" - }] - }, - { - "matcher": "Write", - "hooks": [{ - "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" - }] - }, - { - "matcher": "Read", - "hooks": [{ - "command": "${PAI_DIR}/hooks/SecurityValidator.hook.ts" - }] - } - ] - } -} -``` - ---- - -## Error Handling - -The hook is designed to fail-open for usability: - -| Error | Behavior | -|-------|----------| -| Missing patterns.yaml | Allow all operations | -| YAML parse error | Log warning, allow operation | -| Invalid pattern regex | Try literal match | -| Logging failure | Silent (doesn't block) | - ---- - -## Performance - -- **Blocking:** Yes (must complete before tool executes) -- **Typical execution:** <10ms -- **Design:** Fast path for safe operations, pattern matching only when needed -- **Caching:** Patterns are cached after first load - ---- - -## Testing - -Test the hook directly: - -```bash -# Test a blocked command -echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"},"session_id":"test"}' | \ - bun ~/.opencode/hooks/SecurityValidator.hook.ts -# Should exit 2 (blocked) - -# Test an allowed command -echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"},"session_id":"test"}' | \ - bun ~/.opencode/hooks/SecurityValidator.hook.ts -# Should output {"continue": true} and exit 0 - -# Test a confirm command -echo '{"tool_name":"Bash","tool_input":{"command":"git push --force"},"session_id":"test"}' | \ - bun ~/.opencode/hooks/SecurityValidator.hook.ts -# Should output {"decision": "ask", ...} and exit 0 -``` - ---- - -## Customization - -To add custom patterns: - -1. Create `USER/PAISECURITYSYSTEM/patterns.yaml` (copy from `PAISECURITYSYSTEM/patterns.example.yaml`) -2. Add patterns to appropriate sections -3. Patterns are loaded on next hook invocation (restart session) - -Example custom pattern: -```yaml -bash: - blocked: - - pattern: "npm publish" - reason: "Accidental package publish" -``` diff --git a/.opencode/PAISECURITYSYSTEM/README.md b/.opencode/PAISECURITYSYSTEM/README.md index 1633c497..13f4a0dd 100755 --- a/.opencode/PAISECURITYSYSTEM/README.md +++ b/.opencode/PAISECURITYSYSTEM/README.md @@ -6,7 +6,7 @@ A foundational security framework for Personal AI Infrastructure. ## Two-Layer Design -This directory (`PAISECURITYSYSTEM/`) contains the **base system**—default patterns, documentation, and the security hook. It provides sensible defaults that work out of the box. +This directory (`PAISECURITYSYSTEM/`) contains the **base system**—default patterns, documentation, and the security plugin. It provides sensible defaults that work out of the box. Your personal security policies live in `USER/PAISECURITYSYSTEM/`. This is where you: - Define your own blocked/confirm/alert patterns @@ -14,7 +14,7 @@ Your personal security policies live in `USER/PAISECURITYSYSTEM/`. This is where - Customize path protections - Keep policies that should never be shared publicly -**The hook checks USER first, then falls back to this base system.** This means: +**The plugin checks USER first, then falls back to this base system.** This means: - New PAI users get working security immediately - You can override any default with your own rules - Your personal policies stay private (USER/ is never synced to public PAI) @@ -46,7 +46,7 @@ This security system provides essential protection against catastrophic operatio PAISECURITYSYSTEM/ # System defaults (this directory) ├── README.md # This file ├── ARCHITECTURE.md # Security layer design -├── HOOKS.md # Hook implementation docs +├── PLUGINS.md # Plugin implementation docs ├── PROMPTINJECTION.md # Prompt injection defense ├── COMMANDINJECTION.md # Command injection defense └── patterns.example.yaml # Default security patterns @@ -57,7 +57,7 @@ USER/PAISECURITYSYSTEM/ # Your customizations └── ... # Your additions ``` -The hook loads `USER/PAISECURITYSYSTEM/patterns.yaml` first, falling back to `patterns.example.yaml` if not found. +The plugin loads `USER/PAISECURITYSYSTEM/patterns.yaml` first, falling back to `patterns.example.yaml` if not found. --- @@ -87,7 +87,7 @@ Contributions and feedback welcome. | Document | Purpose | |----------|---------| | `ARCHITECTURE.md` | Security layers, trust hierarchy, philosophy | -| `HOOKS.md` | SecurityValidator implementation details | +| `PLUGINS.md` | SecurityValidator implementation details | | `PROMPTINJECTION.md` | Defense against prompt injection attacks | | `COMMANDINJECTION.md` | Defense against command injection | | `patterns.example.yaml` | Default pattern template |