Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions Packs/pai-hook-system/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,16 @@ PAI_DIR="${PAI_DIR:-$HOME/.claude}"
cp "$PACK_DIR/src/hooks/"*.hook.ts "$PAI_DIR/hooks/"
```

**Hooks included (15 total):**
**Hooks included (17 total):**
- `SecurityValidator.hook.ts` - PreToolUse: Block dangerous commands
- `LoadContext.hook.ts` - SessionStart: Load CORE skill context
- `StartupGreeting.hook.ts` - SessionStart: Voice greeting
- `CheckVersion.hook.ts` - SessionStart: Version compatibility
- `TaskHygiene.hook.ts` - SessionStart: Autonomous cleanup of orphaned tasks
- `UpdateTabTitle.hook.ts` - UserPromptSubmit: Tab automation
- `SetQuestionTab.hook.ts` - UserPromptSubmit: Question tracking
- `ExplicitRatingCapture.hook.ts` - UserPromptSubmit: Rating capture
- `TaskRegistration.hook.ts` - PreToolUse: Register tasks for hygiene tracking
- `FormatEnforcer.hook.ts` - Stop: Format compliance
- `StopOrchestrator.hook.ts` - Stop: Post-response coordination
- `SessionSummary.hook.ts` - Stop: Session summaries
Expand All @@ -264,7 +266,7 @@ PAI_DIR="${PAI_DIR:-$HOME/.claude}"
cp "$PACK_DIR/src/hooks/lib/"*.ts "$PAI_DIR/hooks/lib/"
```

**Libraries included (12 total):**
**Libraries included (13 total):**
- `observability.ts` - Event logging and dashboard integration
- `notifications.ts` - Voice and notification system
- `identity.ts` - Session and user identity
Expand All @@ -277,6 +279,7 @@ cp "$PACK_DIR/src/hooks/lib/"*.ts "$PAI_DIR/hooks/lib/"
- `response-format.ts` - Format validation
- `IdealState.ts` - Goal tracking
- `TraceEmitter.ts` - Trace emission
- `TaskRegistry.ts` - External task tracking with PID liveness enforcement

**Mark todo as completed.**

Expand Down Expand Up @@ -356,7 +359,8 @@ mkdir -p ~/.claude
"hooks": [
{"type": "command", "command": "bun run $PAI_DIR/hooks/StartupGreeting.hook.ts"},
{"type": "command", "command": "bun run $PAI_DIR/hooks/LoadContext.hook.ts"},
{"type": "command", "command": "bun run $PAI_DIR/hooks/CheckVersion.hook.ts"}
{"type": "command", "command": "bun run $PAI_DIR/hooks/CheckVersion.hook.ts"},
{"type": "command", "command": "bun run $PAI_DIR/hooks/TaskHygiene.hook.ts"}
]
}
],
Expand All @@ -366,6 +370,18 @@ mkdir -p ~/.claude
"hooks": [
{"type": "command", "command": "bun run $PAI_DIR/hooks/SecurityValidator.hook.ts"}
]
},
{
"matcher": "TaskCreate",
"hooks": [
{"type": "command", "command": "bun run $PAI_DIR/hooks/TaskRegistration.hook.ts"}
]
},
{
"matcher": "TaskUpdate",
"hooks": [
{"type": "command", "command": "bun run $PAI_DIR/hooks/TaskRegistration.hook.ts"}
]
}
],
"UserPromptSubmit": [
Expand Down Expand Up @@ -416,21 +432,21 @@ PAI_DIR="${PAI_DIR:-$HOME/.claude}"

echo "=== PAI Hook System v2.3.0 Verification ==="

# Check hook files (15 expected)
# Check hook files (17 expected)
echo "Checking hook files..."
HOOK_COUNT=$(ls "$PAI_DIR/hooks/"*.hook.ts 2>/dev/null | wc -l)
echo "Found $HOOK_COUNT hook files (expected: 15)"
echo "Found $HOOK_COUNT hook files (expected: 17)"

# Check critical hooks
[ -f "$PAI_DIR/hooks/SecurityValidator.hook.ts" ] && echo "OK SecurityValidator.hook.ts" || echo "ERROR SecurityValidator.hook.ts missing"
[ -f "$PAI_DIR/hooks/LoadContext.hook.ts" ] && echo "OK LoadContext.hook.ts" || echo "ERROR LoadContext.hook.ts missing"
[ -f "$PAI_DIR/hooks/StopOrchestrator.hook.ts" ] && echo "OK StopOrchestrator.hook.ts" || echo "ERROR StopOrchestrator.hook.ts missing"

# Check library files (12 expected)
# Check library files (13 expected)
echo ""
echo "Checking library files..."
LIB_COUNT=$(ls "$PAI_DIR/hooks/lib/"*.ts 2>/dev/null | wc -l)
echo "Found $LIB_COUNT library files (expected: 12)"
echo "Found $LIB_COUNT library files (expected: 13)"

# Check handler files (4 expected)
echo ""
Expand Down Expand Up @@ -553,14 +569,16 @@ grep "UpdateTabTitle" ~/.claude/settings.json

## What's Included

### Hooks (15 files)
### Hooks (17 files)

| File | Event | Purpose |
|------|-------|---------|
| `SecurityValidator.hook.ts` | PreToolUse | Block dangerous commands |
| `TaskRegistration.hook.ts` | PreToolUse | Register tasks for hygiene tracking |
| `LoadContext.hook.ts` | SessionStart | Load CORE skill context |
| `StartupGreeting.hook.ts` | SessionStart | Voice greeting |
| `CheckVersion.hook.ts` | SessionStart | Version check |
| `TaskHygiene.hook.ts` | SessionStart | Autonomous cleanup of orphaned tasks |
| `UpdateTabTitle.hook.ts` | UserPromptSubmit | Tab automation |
| `SetQuestionTab.hook.ts` | UserPromptSubmit | Question tracking |
| `ExplicitRatingCapture.hook.ts` | UserPromptSubmit | Rating capture |
Expand All @@ -573,7 +591,7 @@ grep "UpdateTabTitle" ~/.claude/settings.json
| `ImplicitSentimentCapture.hook.ts` | Stop | Sentiment analysis |
| `AgentOutputCapture.hook.ts` | SubagentStop | Agent output routing |

### Libraries (12 files)
### Libraries (13 files)

| File | Purpose |
|------|---------|
Expand All @@ -589,6 +607,7 @@ grep "UpdateTabTitle" ~/.claude/settings.json
| `response-format.ts` | Format validation |
| `IdealState.ts` | Goal tracking |
| `TraceEmitter.ts` | Trace emission |
| `TaskRegistry.ts` | External task tracking with PID enforcement |

### Handlers (4 files)

Expand Down
86 changes: 86 additions & 0 deletions Packs/pai-hook-system/src/hooks/TaskHygiene.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bun
/**
* TaskHygiene.hook.ts - Autonomous stale task cleanup with ENFORCEMENT (SessionStart)
*
* PURPOSE:
* Detects orphaned tasks by checking if their owning process is still alive.
* Uses factual PID liveness checks (kill -0) instead of documentation-based reminders.
*
* TRIGGER: SessionStart
*
* DESIGN PHILOSOPHY (from RedTeam analysis):
* - Documentation-based cleanup ("please clean up stale tasks") has ZERO enforcement power
* - This hook uses an external TaskRegistry that tracks task -> PID mappings
* - At session start, checks each registered task's PID with `kill -0`
* - Dead PID = orphaned task = FACTUAL information for Claude to act on
*
* OUTPUT:
* - If orphans found: Outputs specific task IDs with VERIFIED dead PIDs
* - If no orphans: Silent exit
* - Claude receives FACTS, not suggestions
*/

import { findOrphanedTasks, cleanOrphanedTasks, getAllTasks, TaskEntry } from './lib/TaskRegistry';
import { getPaiDir } from './lib/paths';

async function main() {
try {
// Skip for subagent sessions
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
const isSubagent = claudeProjectDir.includes('/.claude/Agents/') ||
process.env.CLAUDE_AGENT_TYPE !== undefined;

if (isSubagent) {
console.error('🤖 Subagent session - skipping task hygiene');
process.exit(0);
}

console.error('🧹 Checking for orphaned tasks...');

// Get all registered tasks and find orphans
const allTasks = getAllTasks();
const orphans = findOrphanedTasks();

console.error(`📊 Registry: ${allTasks.length} tasks, ${orphans.length} orphans`);

if (orphans.length === 0) {
console.error('✅ No orphaned tasks found');
process.exit(0);
}

// Clean orphans from registry
const cleanedIds = cleanOrphanedTasks();
console.error(`🗑️ Cleaned ${cleanedIds.length} orphans from registry`);

// Format orphan information for Claude
const orphanDetails = orphans.map((o: TaskEntry) =>
` - Task "${o.taskId}": subject="${o.subject}", PID ${o.pid} (DEAD), created ${o.createdAt}`
).join('\n');

// Output FACTUAL orphan report - Claude must act on verified information
const report = `<system-reminder>
TASK HYGIENE: VERIFIED ORPHANED TASKS DETECTED

The following tasks have DEAD owning processes (verified via kill -0):

${orphanDetails}

**ACTION REQUIRED:**
These task IDs must be deleted using TaskUpdate with status='deleted':
${cleanedIds.map(id => `- TaskUpdate(taskId="${id}", status="deleted")`).join('\n')}

This is NOT a suggestion. The owning processes are confirmed dead.
Execute cleanup before proceeding with other work.
</system-reminder>`;

console.log(report);
console.error('⚠️ Orphaned tasks detected - cleanup instructions emitted');
process.exit(0);
} catch (error) {
console.error('⚠️ Task hygiene hook error:', error);
// Non-fatal - don't block session start
process.exit(0);
}
}

main();
86 changes: 86 additions & 0 deletions Packs/pai-hook-system/src/hooks/TaskRegistration.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bun
/**
* TaskRegistration.hook.ts - Register tasks in external registry (PreToolUse: TaskCreate/TaskUpdate)
*
* PURPOSE:
* Tracks task creation and completion in an external registry that hooks can access.
* Enables SessionStart hygiene hook to detect orphans via PID liveness checks.
*
* TRIGGERS:
* - PreToolUse: TaskCreate → Register new task with current PID
* - PreToolUse: TaskUpdate → Update heartbeat or unregister on completion/deletion
*
* DESIGN:
* This hook reads the tool input from stdin and:
* - For TaskCreate: Extracts subject, registers with current PID
* - For TaskUpdate with status=completed/deleted: Unregisters task
* - For TaskUpdate with other changes: Updates heartbeat
*
* The registry enables the SessionStart hygiene hook to check `kill -0 $pid`
* and detect when a task's owning process is dead (crashed agent = orphan).
*/

import { registerTask, unregisterTask, heartbeatTask } from './lib/TaskRegistry';

interface HookInput {
tool_name: string;
tool_input: {
subject?: string;
taskId?: string;
status?: string;
description?: string;
activeForm?: string;
};
}

async function main() {
try {
// Read hook input from stdin
const input = await Bun.stdin.text();
if (!input.trim()) {
process.exit(0);
}

const hookData: HookInput = JSON.parse(input);
const { tool_name, tool_input } = hookData;

// Determine if this is a subagent
const claudeProjectDir = process.env.CLAUDE_PROJECT_DIR || '';
const isSubagent = claudeProjectDir.includes('/.claude/Agents/') ||
process.env.CLAUDE_AGENT_TYPE !== undefined;

if (tool_name === 'TaskCreate') {
// Register new task with current process info
const taskId = `pending-${Date.now()}`; // Temporary ID until actual ID is assigned
const subject = tool_input.subject || 'Unknown task';

registerTask(taskId, subject, isSubagent);
console.error(`📝 Registered task: ${subject} (PID: ${process.pid}, subagent: ${isSubagent})`);

} else if (tool_name === 'TaskUpdate') {
const { taskId, status } = tool_input;

if (!taskId) {
process.exit(0);
}

if (status === 'completed' || status === 'deleted') {
// Task is done - remove from registry
unregisterTask(taskId);
console.error(`🗑️ Unregistered task: ${taskId} (status: ${status})`);
} else {
// Task is being updated - refresh heartbeat
heartbeatTask(taskId);
console.error(`💓 Heartbeat updated: ${taskId}`);
}
}

process.exit(0);
} catch (error) {
// Non-fatal - don't block task operations
console.error(`⚠️ Task registration hook error: ${error}`);
process.exit(0);
}
}

main();
Loading