panopticon-cli 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/heartbeat-hook +149 -0
- package/scripts/install-git-hooks.sh +66 -0
- package/scripts/notify-complete +79 -0
- package/scripts/postinstall.mjs +49 -0
- package/scripts/pre-tool-hook +60 -0
- package/scripts/record-cost-event.js +94 -0
- package/scripts/record-cost-event.ts +113 -0
- package/scripts/restart-dashboard.sh +59 -0
- package/scripts/setup-certs.sh +66 -0
- package/scripts/specialist-stop-hook +106 -0
- package/scripts/stop-hook +58 -0
- package/scripts/validate-merge.sh +119 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panopticon-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dist",
|
|
40
40
|
"templates",
|
|
41
41
|
"skills",
|
|
42
|
-
"scripts
|
|
42
|
+
"scripts",
|
|
43
43
|
"README.md",
|
|
44
44
|
"LICENSE"
|
|
45
45
|
],
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/heartbeat-hook
|
|
3
|
+
# Called by Claude Code after every tool use with JSON on stdin
|
|
4
|
+
#
|
|
5
|
+
# This hook receives PostToolUse event data from Claude Code and writes
|
|
6
|
+
# rich heartbeat information to enable real-time agent monitoring.
|
|
7
|
+
|
|
8
|
+
# Don't use set -e - we want the hook to be resilient to failures
|
|
9
|
+
# and never break Claude Code execution
|
|
10
|
+
|
|
11
|
+
# Parse tool info from stdin
|
|
12
|
+
TOOL_INFO=$(cat 2>/dev/null || echo '{}')
|
|
13
|
+
|
|
14
|
+
# Check if jq is available
|
|
15
|
+
if ! command -v jq &> /dev/null; then
|
|
16
|
+
echo "Warning: jq not found. Heartbeat hook requires jq to parse tool data." >&2
|
|
17
|
+
exit 0 # Silent failure - don't break Claude Code execution
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Extract tool name and input (truncate input to 100 chars to avoid huge files)
|
|
21
|
+
TOOL_NAME=$(echo "$TOOL_INFO" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo "unknown")
|
|
22
|
+
TOOL_INPUT=$(echo "$TOOL_INFO" | jq -r '.tool_input | tostring | .[0:100] // ""' 2>/dev/null || echo "")
|
|
23
|
+
|
|
24
|
+
# Get agent ID from env (set by pan work issue) or tmux session name
|
|
25
|
+
# Only use tmux session name if we're actually INSIDE a tmux session ($TMUX is set)
|
|
26
|
+
if [ -n "$PANOPTICON_AGENT_ID" ]; then
|
|
27
|
+
AGENT_ID="$PANOPTICON_AGENT_ID"
|
|
28
|
+
elif [ -n "$TMUX" ]; then
|
|
29
|
+
AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
|
|
30
|
+
else
|
|
31
|
+
AGENT_ID="main-cli"
|
|
32
|
+
fi
|
|
33
|
+
AGENT_ID="${AGENT_ID:-unknown}"
|
|
34
|
+
|
|
35
|
+
# Get current beads task from cache (if exists)
|
|
36
|
+
TASK_CACHE="$HOME/.panopticon/agents/$AGENT_ID/current-task.json"
|
|
37
|
+
CURRENT_TASK=""
|
|
38
|
+
if [ -f "$TASK_CACHE" ]; then
|
|
39
|
+
CURRENT_TASK=$(jq -r '.title // ""' "$TASK_CACHE" 2>/dev/null || echo "")
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Get git branch (fast, single command) - suppress all errors
|
|
43
|
+
GIT_BRANCH=$(git branch --show-current 2>/dev/null || true)
|
|
44
|
+
GIT_BRANCH="${GIT_BRANCH:-unknown}"
|
|
45
|
+
|
|
46
|
+
# Get workspace from pwd
|
|
47
|
+
WORKSPACE=$(pwd)
|
|
48
|
+
|
|
49
|
+
# Ensure heartbeat directory exists
|
|
50
|
+
HEARTBEAT_DIR="$HOME/.panopticon/heartbeats"
|
|
51
|
+
mkdir -p "$HEARTBEAT_DIR"
|
|
52
|
+
|
|
53
|
+
# Ensure agent state directory exists for activity log
|
|
54
|
+
AGENT_DIR="$HOME/.panopticon/agents/$AGENT_ID"
|
|
55
|
+
mkdir -p "$AGENT_DIR"
|
|
56
|
+
|
|
57
|
+
# Write heartbeat atomically (write to temp file, then move)
|
|
58
|
+
# Use jq to properly escape all strings in the JSON output
|
|
59
|
+
TEMP_FILE="$HEARTBEAT_DIR/$AGENT_ID.json.tmp"
|
|
60
|
+
jq -n \
|
|
61
|
+
--arg timestamp "$(date -Iseconds)" \
|
|
62
|
+
--arg agent_id "$AGENT_ID" \
|
|
63
|
+
--arg tool_name "$TOOL_NAME" \
|
|
64
|
+
--arg last_action "$TOOL_INPUT" \
|
|
65
|
+
--arg current_task "$CURRENT_TASK" \
|
|
66
|
+
--arg git_branch "$GIT_BRANCH" \
|
|
67
|
+
--arg workspace "$WORKSPACE" \
|
|
68
|
+
--argjson pid $$ \
|
|
69
|
+
'{
|
|
70
|
+
timestamp: $timestamp,
|
|
71
|
+
agent_id: $agent_id,
|
|
72
|
+
tool_name: $tool_name,
|
|
73
|
+
last_action: $last_action,
|
|
74
|
+
current_task: $current_task,
|
|
75
|
+
git_branch: $git_branch,
|
|
76
|
+
workspace: $workspace,
|
|
77
|
+
pid: $pid
|
|
78
|
+
}' > "$TEMP_FILE" 2>/dev/null || true
|
|
79
|
+
|
|
80
|
+
# Atomic move to avoid race conditions
|
|
81
|
+
mv "$TEMP_FILE" "$HEARTBEAT_DIR/$AGENT_ID.json" 2>/dev/null || true
|
|
82
|
+
|
|
83
|
+
# Append to activity log (JSONL format)
|
|
84
|
+
ACTIVITY_FILE="$AGENT_DIR/activity.jsonl"
|
|
85
|
+
|
|
86
|
+
# Create activity entry
|
|
87
|
+
ACTIVITY_ENTRY=$(jq -n \
|
|
88
|
+
--arg ts "$(date -Iseconds)" \
|
|
89
|
+
--arg tool "$TOOL_NAME" \
|
|
90
|
+
--arg action "$TOOL_INPUT" \
|
|
91
|
+
'{
|
|
92
|
+
ts: $ts,
|
|
93
|
+
tool: $tool,
|
|
94
|
+
action: $action
|
|
95
|
+
}' 2>/dev/null || echo "")
|
|
96
|
+
|
|
97
|
+
# Append to activity log if we successfully created the entry
|
|
98
|
+
if [ -n "$ACTIVITY_ENTRY" ]; then
|
|
99
|
+
echo "$ACTIVITY_ENTRY" >> "$ACTIVITY_FILE" 2>/dev/null || true
|
|
100
|
+
|
|
101
|
+
# Prune to last 100 entries (use tail for atomic operation)
|
|
102
|
+
if [ -f "$ACTIVITY_FILE" ]; then
|
|
103
|
+
TEMP_ACTIVITY="$ACTIVITY_FILE.tmp"
|
|
104
|
+
tail -n 100 "$ACTIVITY_FILE" > "$TEMP_ACTIVITY" 2>/dev/null && mv "$TEMP_ACTIVITY" "$ACTIVITY_FILE" 2>/dev/null || true
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Update agent runtime state with latest activity timestamp (PAN-119)
|
|
109
|
+
STATE_FILE="$AGENT_DIR/state.json"
|
|
110
|
+
TIMESTAMP="$(date -Iseconds)"
|
|
111
|
+
|
|
112
|
+
if [ -f "$STATE_FILE" ]; then
|
|
113
|
+
# Update existing state file's lastActivity field
|
|
114
|
+
TEMP_STATE="$STATE_FILE.tmp"
|
|
115
|
+
jq --arg ts "$TIMESTAMP" --arg tool "$TOOL_NAME" \
|
|
116
|
+
'.lastActivity = $ts | .currentTool = $tool | .state = "active"' \
|
|
117
|
+
"$STATE_FILE" > "$TEMP_STATE" 2>/dev/null && mv "$TEMP_STATE" "$STATE_FILE" 2>/dev/null || true
|
|
118
|
+
else
|
|
119
|
+
# Create initial state file
|
|
120
|
+
jq -n \
|
|
121
|
+
--arg ts "$TIMESTAMP" \
|
|
122
|
+
--arg tool "$TOOL_NAME" \
|
|
123
|
+
'{
|
|
124
|
+
state: "active",
|
|
125
|
+
lastActivity: $ts,
|
|
126
|
+
currentTool: $tool
|
|
127
|
+
}' > "$STATE_FILE" 2>/dev/null || true
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Record cost event from tool usage (PAN-81)
|
|
131
|
+
# Find the record-cost-event.js script - check multiple locations
|
|
132
|
+
COST_SCRIPT=""
|
|
133
|
+
if [ -f "$HOME/.panopticon/bin/record-cost-event.js" ]; then
|
|
134
|
+
COST_SCRIPT="$HOME/.panopticon/bin/record-cost-event.js"
|
|
135
|
+
elif [ -f "$(dirname "$0")/record-cost-event.js" ]; then
|
|
136
|
+
COST_SCRIPT="$(dirname "$0")/record-cost-event.js"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if [ -n "$COST_SCRIPT" ] && [ -f "$COST_SCRIPT" ]; then
|
|
140
|
+
# Export issue context for the cost recording script
|
|
141
|
+
export PANOPTICON_ISSUE_ID="${PANOPTICON_ISSUE_ID:-UNKNOWN}"
|
|
142
|
+
export PANOPTICON_SESSION_TYPE="${PANOPTICON_SESSION_TYPE:-implementation}"
|
|
143
|
+
|
|
144
|
+
# Call cost recording script with tool info on stdin
|
|
145
|
+
echo "$TOOL_INFO" | node "$COST_SCRIPT" 2>/dev/null || true
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Always exit successfully - never break Claude Code execution
|
|
149
|
+
exit 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Install Panopticon git hooks in a project's repos
|
|
4
|
+
# Usage: ./install-git-hooks.sh /path/to/project
|
|
5
|
+
#
|
|
6
|
+
# For poly-repos, this will find all .git directories and install hooks in each.
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
HOOKS_DIR="$SCRIPT_DIR/git-hooks"
|
|
12
|
+
TARGET_DIR="${1:-.}"
|
|
13
|
+
|
|
14
|
+
if [ ! -d "$TARGET_DIR" ]; then
|
|
15
|
+
echo "Error: Directory does not exist: $TARGET_DIR"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "Installing Panopticon git hooks in: $TARGET_DIR"
|
|
20
|
+
echo ""
|
|
21
|
+
|
|
22
|
+
# Find all .git directories (not files - those are worktrees)
|
|
23
|
+
# Also exclude node_modules, target, etc.
|
|
24
|
+
find "$TARGET_DIR" -maxdepth 4 -type d -name ".git" \
|
|
25
|
+
-not -path "*/node_modules/*" \
|
|
26
|
+
-not -path "*/target/*" \
|
|
27
|
+
-not -path "*/.git/*" \
|
|
28
|
+
-not -path "*/workspaces/*" \
|
|
29
|
+
2>/dev/null | while read git_dir; do
|
|
30
|
+
|
|
31
|
+
hooks_target="$git_dir/hooks"
|
|
32
|
+
repo_dir="$(dirname "$git_dir")"
|
|
33
|
+
|
|
34
|
+
echo "Installing hooks in: $repo_dir"
|
|
35
|
+
|
|
36
|
+
# Create hooks directory if it doesn't exist
|
|
37
|
+
mkdir -p "$hooks_target"
|
|
38
|
+
|
|
39
|
+
# Install each hook
|
|
40
|
+
for hook in "$HOOKS_DIR"/*; do
|
|
41
|
+
if [ -f "$hook" ]; then
|
|
42
|
+
hook_name=$(basename "$hook")
|
|
43
|
+
target_hook="$hooks_target/$hook_name"
|
|
44
|
+
|
|
45
|
+
# Check if hook already exists
|
|
46
|
+
if [ -f "$target_hook" ] && [ ! -L "$target_hook" ]; then
|
|
47
|
+
echo " ⚠️ $hook_name: existing hook found, creating backup"
|
|
48
|
+
mv "$target_hook" "$target_hook.backup"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Create symlink to our hook
|
|
52
|
+
ln -sf "$hook" "$target_hook"
|
|
53
|
+
echo " ✓ $hook_name installed"
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
echo ""
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
echo "Done! Git hooks installed."
|
|
61
|
+
echo ""
|
|
62
|
+
echo "The post-checkout hook will warn if the main project directory"
|
|
63
|
+
echo "is checked out to a branch other than 'main'."
|
|
64
|
+
echo ""
|
|
65
|
+
echo "To enable auto-revert (automatically switch back to main):"
|
|
66
|
+
echo " export PANOPTICON_AUTO_REVERT_CHECKOUT=1"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# notify-complete: Send desktop notification when agent completes work
|
|
3
|
+
# Part of Panopticon - works on WSL2/Windows, Linux, and macOS
|
|
4
|
+
#
|
|
5
|
+
# Usage: notify-complete <issue-id> <title> [mr-url]
|
|
6
|
+
#
|
|
7
|
+
# Examples:
|
|
8
|
+
# notify-complete MIN-665 "Fixed login button"
|
|
9
|
+
# notify-complete PAN-96 "Implemented templates" "https://gitlab.com/mr/123"
|
|
10
|
+
|
|
11
|
+
ISSUE_ID="${1:-UNKNOWN}"
|
|
12
|
+
TITLE="${2:-Agent completed work}"
|
|
13
|
+
MR_URL="${3:-}"
|
|
14
|
+
|
|
15
|
+
# Log to completion file
|
|
16
|
+
COMPLETION_LOG="$HOME/.panopticon/agent-completed.log"
|
|
17
|
+
mkdir -p "$(dirname "$COMPLETION_LOG")"
|
|
18
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') | ${ISSUE_ID} | ${TITLE} | ${MR_URL}" >> "$COMPLETION_LOG"
|
|
19
|
+
|
|
20
|
+
# Detect platform and send notification
|
|
21
|
+
send_notification() {
|
|
22
|
+
local title="$1"
|
|
23
|
+
local message="$2"
|
|
24
|
+
|
|
25
|
+
# WSL2/Windows - use PowerShell toast notifications
|
|
26
|
+
if command -v powershell.exe &>/dev/null; then
|
|
27
|
+
powershell.exe -Command "
|
|
28
|
+
\$xml = @\"
|
|
29
|
+
<toast>
|
|
30
|
+
<visual>
|
|
31
|
+
<binding template=\"ToastText02\">
|
|
32
|
+
<text id=\"1\">${title}</text>
|
|
33
|
+
<text id=\"2\">${message}</text>
|
|
34
|
+
</binding>
|
|
35
|
+
</visual>
|
|
36
|
+
<audio src=\"ms-winsoundevent:Notification.Default\"/>
|
|
37
|
+
</toast>
|
|
38
|
+
\"@
|
|
39
|
+
|
|
40
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
41
|
+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
|
42
|
+
|
|
43
|
+
\$template = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
44
|
+
\$template.LoadXml(\$xml)
|
|
45
|
+
\$toast = New-Object Windows.UI.Notifications.ToastNotification \$template
|
|
46
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Panopticon').Show(\$toast)
|
|
47
|
+
" 2>/dev/null && return 0
|
|
48
|
+
|
|
49
|
+
# Fallback: MessageBox
|
|
50
|
+
powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${message}', '${title}', 'OK', 'Information')" 2>/dev/null &
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# macOS - use osascript
|
|
55
|
+
if command -v osascript &>/dev/null; then
|
|
56
|
+
osascript -e "display notification \"${message}\" with title \"${title}\"" 2>/dev/null
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Linux - use notify-send
|
|
61
|
+
if command -v notify-send &>/dev/null; then
|
|
62
|
+
notify-send "${title}" "${message}" 2>/dev/null
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Fallback - just print to console
|
|
67
|
+
echo "[NOTIFICATION] ${title}: ${message}"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Build notification message
|
|
71
|
+
if [ -n "$MR_URL" ]; then
|
|
72
|
+
MESSAGE="${TITLE} - Ready for review"
|
|
73
|
+
else
|
|
74
|
+
MESSAGE="${TITLE} - Work complete"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
send_notification "Panopticon: ${ISSUE_ID}" "$MESSAGE"
|
|
78
|
+
|
|
79
|
+
echo "Notification sent for ${ISSUE_ID}"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script for Panopticon
|
|
4
|
+
*
|
|
5
|
+
* Automatically syncs hooks after npm install/upgrade if Panopticon
|
|
6
|
+
* has been initialized (bin dir exists).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, copyFileSync, chmodSync, mkdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const BIN_DIR = join(homedir(), '.panopticon', 'bin');
|
|
16
|
+
const SCRIPTS_DIR = __dirname;
|
|
17
|
+
|
|
18
|
+
// Only run if Panopticon has been initialized
|
|
19
|
+
if (!existsSync(join(homedir(), '.panopticon'))) {
|
|
20
|
+
console.log('Panopticon not initialized yet. Run `pan init` to set up.');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Ensure bin directory exists
|
|
25
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
26
|
+
|
|
27
|
+
// Copy all scripts from scripts/ to ~/.panopticon/bin/
|
|
28
|
+
const scripts = readdirSync(SCRIPTS_DIR)
|
|
29
|
+
.filter(f => !f.startsWith('.') && !f.endsWith('.mjs') && !f.endsWith('.js'));
|
|
30
|
+
|
|
31
|
+
let synced = 0;
|
|
32
|
+
for (const script of scripts) {
|
|
33
|
+
try {
|
|
34
|
+
const source = join(SCRIPTS_DIR, script);
|
|
35
|
+
const target = join(BIN_DIR, script);
|
|
36
|
+
copyFileSync(source, target);
|
|
37
|
+
chmodSync(target, 0o755);
|
|
38
|
+
synced++;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Ignore errors, hooks are non-critical
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (synced > 0) {
|
|
45
|
+
console.log(`✓ Synced ${synced} hooks to ~/.panopticon/bin/`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Suggest running full sync
|
|
49
|
+
console.log('Run `pan sync` to sync skills and commands to AI tools.');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/pre-tool-hook
|
|
3
|
+
# Called by Claude Code before every tool use
|
|
4
|
+
#
|
|
5
|
+
# This hook sets agent state to "active" and optionally sends
|
|
6
|
+
# heartbeat to API server for real-time dashboard updates
|
|
7
|
+
|
|
8
|
+
# Don't use set -e - we want the hook to be resilient to failures
|
|
9
|
+
# and never break Claude Code execution
|
|
10
|
+
|
|
11
|
+
# Get agent ID from env (set by pan work issue) or tmux session name
|
|
12
|
+
if [ -n "$PANOPTICON_AGENT_ID" ]; then
|
|
13
|
+
AGENT_ID="$PANOPTICON_AGENT_ID"
|
|
14
|
+
elif [ -n "$TMUX" ]; then
|
|
15
|
+
AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
|
|
16
|
+
else
|
|
17
|
+
AGENT_ID="main-cli"
|
|
18
|
+
fi
|
|
19
|
+
AGENT_ID="${AGENT_ID:-unknown}"
|
|
20
|
+
|
|
21
|
+
# Parse tool info from stdin
|
|
22
|
+
TOOL_INFO=$(cat 2>/dev/null || echo '{}')
|
|
23
|
+
|
|
24
|
+
# Check if jq is available
|
|
25
|
+
if ! command -v jq &> /dev/null; then
|
|
26
|
+
exit 0 # Silent failure - don't break Claude Code execution
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Extract tool name
|
|
30
|
+
TOOL_NAME=$(echo "$TOOL_INFO" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo "unknown")
|
|
31
|
+
|
|
32
|
+
# Ensure state directory exists
|
|
33
|
+
STATE_DIR="$HOME/.panopticon/agents/$AGENT_ID"
|
|
34
|
+
mkdir -p "$STATE_DIR"
|
|
35
|
+
|
|
36
|
+
# Write state to file (atomic write via temp file)
|
|
37
|
+
TEMP_FILE="$STATE_DIR/state.json.tmp"
|
|
38
|
+
jq -n \
|
|
39
|
+
--arg timestamp "$(date -Iseconds)" \
|
|
40
|
+
--arg state "active" \
|
|
41
|
+
--arg tool "$TOOL_NAME" \
|
|
42
|
+
'{
|
|
43
|
+
state: $state,
|
|
44
|
+
lastActivity: $timestamp,
|
|
45
|
+
currentTool: $tool
|
|
46
|
+
}' > "$TEMP_FILE" 2>/dev/null || true
|
|
47
|
+
|
|
48
|
+
mv "$TEMP_FILE" "$STATE_DIR/state.json" 2>/dev/null || true
|
|
49
|
+
|
|
50
|
+
# Optionally send heartbeat to API server (non-blocking)
|
|
51
|
+
# Only if dashboard is running
|
|
52
|
+
if curl -s -f --max-time 0.5 "http://localhost:3011/health" > /dev/null 2>&1; then
|
|
53
|
+
curl -s -X POST "http://localhost:3011/api/agents/$AGENT_ID/heartbeat" \
|
|
54
|
+
-H "Content-Type: application/json" \
|
|
55
|
+
-d "{\"state\":\"active\",\"tool\":\"$TOOL_NAME\",\"timestamp\":\"$(date -Iseconds)\"}" \
|
|
56
|
+
> /dev/null 2>&1 &
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Always exit successfully
|
|
60
|
+
exit 0
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Record a cost event from Claude Code tool usage
|
|
4
|
+
* Called by heartbeat-hook with JSON input on stdin
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { calculateCost, getPricing } from '../src/lib/cost.js';
|
|
9
|
+
import { appendCostEvent } from '../src/lib/costs/events.js';
|
|
10
|
+
|
|
11
|
+
// Read tool info from stdin
|
|
12
|
+
let toolInfo;
|
|
13
|
+
try {
|
|
14
|
+
const input = readFileSync(0, 'utf-8');
|
|
15
|
+
toolInfo = JSON.parse(input);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
// Silent failure - don't break Claude Code execution
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Extract usage data from tool info
|
|
22
|
+
const usage = toolInfo?.usage || toolInfo?.message?.usage;
|
|
23
|
+
if (!usage) {
|
|
24
|
+
// No usage data - not a Claude API call
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extract token counts
|
|
29
|
+
const inputTokens = usage.input_tokens || 0;
|
|
30
|
+
const outputTokens = usage.output_tokens || 0;
|
|
31
|
+
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
32
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
|
|
33
|
+
|
|
34
|
+
// Must have at least some tokens to record
|
|
35
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract model name
|
|
40
|
+
const model = toolInfo?.model || toolInfo?.message?.model || 'claude-sonnet-4';
|
|
41
|
+
|
|
42
|
+
// Determine provider from model name
|
|
43
|
+
let provider = 'anthropic';
|
|
44
|
+
if (model.includes('gpt')) {
|
|
45
|
+
provider = 'openai';
|
|
46
|
+
} else if (model.includes('gemini')) {
|
|
47
|
+
provider = 'google';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get pricing and calculate cost
|
|
51
|
+
const pricing = getPricing(provider, model);
|
|
52
|
+
if (!pricing) {
|
|
53
|
+
console.warn(`No pricing found for ${provider}/${model}`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cost = calculateCost({
|
|
58
|
+
inputTokens,
|
|
59
|
+
outputTokens,
|
|
60
|
+
cacheReadTokens,
|
|
61
|
+
cacheWriteTokens,
|
|
62
|
+
cacheTTL: '5m',
|
|
63
|
+
}, pricing);
|
|
64
|
+
|
|
65
|
+
// Get agent and issue context from environment
|
|
66
|
+
// PANOPTICON_AGENT_ID should always be set by pan work or heartbeat-hook
|
|
67
|
+
// If not set, use a fallback that makes it clear costs are unattributed
|
|
68
|
+
const agentId = process.env.PANOPTICON_AGENT_ID || 'unattributed';
|
|
69
|
+
|
|
70
|
+
const issueId = process.env.PANOPTICON_ISSUE_ID || 'UNKNOWN';
|
|
71
|
+
const sessionType = process.env.PANOPTICON_SESSION_TYPE || 'implementation';
|
|
72
|
+
|
|
73
|
+
// Record cost event
|
|
74
|
+
try {
|
|
75
|
+
appendCostEvent({
|
|
76
|
+
ts: new Date().toISOString(),
|
|
77
|
+
type: 'cost',
|
|
78
|
+
agentId,
|
|
79
|
+
issueId,
|
|
80
|
+
sessionType,
|
|
81
|
+
provider,
|
|
82
|
+
model,
|
|
83
|
+
input: inputTokens,
|
|
84
|
+
output: outputTokens,
|
|
85
|
+
cacheRead: cacheReadTokens,
|
|
86
|
+
cacheWrite: cacheWriteTokens,
|
|
87
|
+
cost,
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Silent failure - don't break Claude Code execution
|
|
91
|
+
console.error('Failed to record cost event:', err);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.exit(0);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Record a cost event from Claude Code tool usage
|
|
4
|
+
* Called by heartbeat-hook with JSON input on stdin
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { calculateCost, getPricing, AIProvider } from '../src/lib/cost.js';
|
|
9
|
+
import { appendCostEvent } from '../src/lib/costs/events.js';
|
|
10
|
+
|
|
11
|
+
// ============== Types ==============
|
|
12
|
+
|
|
13
|
+
interface UsageData {
|
|
14
|
+
input_tokens?: number;
|
|
15
|
+
output_tokens?: number;
|
|
16
|
+
cache_read_input_tokens?: number;
|
|
17
|
+
cache_creation_input_tokens?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ToolInfo {
|
|
21
|
+
model?: string;
|
|
22
|
+
usage?: UsageData;
|
|
23
|
+
message?: {
|
|
24
|
+
model?: string;
|
|
25
|
+
usage?: UsageData;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============== Main ==============
|
|
30
|
+
|
|
31
|
+
// Read tool info from stdin
|
|
32
|
+
let toolInfo: ToolInfo;
|
|
33
|
+
try {
|
|
34
|
+
const input = readFileSync(0, 'utf-8');
|
|
35
|
+
toolInfo = JSON.parse(input) as ToolInfo;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// Silent failure - don't break Claude Code execution
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract usage data from tool info
|
|
42
|
+
const usage: UsageData | undefined = toolInfo?.usage || toolInfo?.message?.usage;
|
|
43
|
+
if (!usage) {
|
|
44
|
+
// No usage data - not a Claude API call
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract token counts
|
|
49
|
+
const inputTokens = usage.input_tokens || 0;
|
|
50
|
+
const outputTokens = usage.output_tokens || 0;
|
|
51
|
+
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
52
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
|
|
53
|
+
|
|
54
|
+
// Must have at least some tokens to record
|
|
55
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) {
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract model name
|
|
60
|
+
const model: string = toolInfo?.model || toolInfo?.message?.model || 'claude-sonnet-4';
|
|
61
|
+
|
|
62
|
+
// Determine provider from model name
|
|
63
|
+
let provider: AIProvider = 'anthropic';
|
|
64
|
+
if (model.includes('gpt')) {
|
|
65
|
+
provider = 'openai';
|
|
66
|
+
} else if (model.includes('gemini')) {
|
|
67
|
+
provider = 'google';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get pricing and calculate cost
|
|
71
|
+
const pricing = getPricing(provider, model);
|
|
72
|
+
if (!pricing) {
|
|
73
|
+
console.warn(`No pricing found for ${provider}/${model}`);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cost = calculateCost({
|
|
78
|
+
inputTokens,
|
|
79
|
+
outputTokens,
|
|
80
|
+
cacheReadTokens,
|
|
81
|
+
cacheWriteTokens,
|
|
82
|
+
cacheTTL: '5m',
|
|
83
|
+
}, pricing);
|
|
84
|
+
|
|
85
|
+
// Get agent and issue context from environment
|
|
86
|
+
// PANOPTICON_AGENT_ID should always be set by pan work or heartbeat-hook
|
|
87
|
+
// If not set, use a fallback that makes it clear costs are unattributed
|
|
88
|
+
const agentId: string = process.env.PANOPTICON_AGENT_ID || 'unattributed';
|
|
89
|
+
const issueId: string = process.env.PANOPTICON_ISSUE_ID || 'UNKNOWN';
|
|
90
|
+
const sessionType: string = process.env.PANOPTICON_SESSION_TYPE || 'implementation';
|
|
91
|
+
|
|
92
|
+
// Record cost event
|
|
93
|
+
try {
|
|
94
|
+
appendCostEvent({
|
|
95
|
+
ts: new Date().toISOString(),
|
|
96
|
+
type: 'cost',
|
|
97
|
+
agentId,
|
|
98
|
+
issueId,
|
|
99
|
+
sessionType,
|
|
100
|
+
provider,
|
|
101
|
+
model,
|
|
102
|
+
input: inputTokens,
|
|
103
|
+
output: outputTokens,
|
|
104
|
+
cacheRead: cacheReadTokens,
|
|
105
|
+
cacheWrite: cacheWriteTokens,
|
|
106
|
+
cost,
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
// Silent failure - don't break Claude Code execution
|
|
110
|
+
console.error('Failed to record cost event:', err);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.exit(0);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Restart Panopticon dashboard cleanly
|
|
3
|
+
# Usage: ./restart-dashboard.sh
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
DASHBOARD_DIR="$SCRIPT_DIR/../src/dashboard"
|
|
9
|
+
LOG_FILE="/tmp/panopticon-dashboard.log"
|
|
10
|
+
|
|
11
|
+
echo "Stopping dashboard..."
|
|
12
|
+
|
|
13
|
+
# Most reliable: kill by port using fuser -k
|
|
14
|
+
for port in 3010 3011 3012; do
|
|
15
|
+
fuser -k ${port}/tcp 2>/dev/null || true
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
# Also kill any npm/node processes that might be orphaned
|
|
19
|
+
pkill -9 -f "npm.*dashboard" 2>/dev/null || true
|
|
20
|
+
pkill -9 -f "node.*panopticon.*dashboard" 2>/dev/null || true
|
|
21
|
+
pkill -9 -f "vite.*301" 2>/dev/null || true
|
|
22
|
+
pkill -9 -f "concurrently.*dev:server" 2>/dev/null || true
|
|
23
|
+
|
|
24
|
+
sleep 2
|
|
25
|
+
|
|
26
|
+
# Verify ports are clear
|
|
27
|
+
if lsof -i :3010,:3011,:3012 >/dev/null 2>&1; then
|
|
28
|
+
echo "Warning: Ports still in use, force killing..."
|
|
29
|
+
lsof -ti :3010,:3011,:3012 | xargs -r kill -9 2>/dev/null || true
|
|
30
|
+
sleep 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
echo "Starting dashboard..."
|
|
34
|
+
|
|
35
|
+
cd "$DASHBOARD_DIR"
|
|
36
|
+
rm -f "$LOG_FILE"
|
|
37
|
+
|
|
38
|
+
# Use setsid to fully detach from terminal
|
|
39
|
+
setsid npm run dev > "$LOG_FILE" 2>&1 &
|
|
40
|
+
|
|
41
|
+
# Wait for API to be ready
|
|
42
|
+
echo -n "Waiting for API"
|
|
43
|
+
for i in {1..30}; do
|
|
44
|
+
if curl -s --max-time 2 http://localhost:3011/api/health > /dev/null 2>&1; then
|
|
45
|
+
echo ""
|
|
46
|
+
echo "Dashboard ready!"
|
|
47
|
+
echo " Frontend: http://localhost:3010"
|
|
48
|
+
echo " API: http://localhost:3011"
|
|
49
|
+
echo " Logs: $LOG_FILE"
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
echo -n "."
|
|
53
|
+
sleep 1
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
echo ""
|
|
57
|
+
echo "ERROR: API not responding after 30s"
|
|
58
|
+
echo "Check logs: tail -50 $LOG_FILE"
|
|
59
|
+
exit 1
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Setup local HTTPS certificates for Panopticon using mkcert
|
|
3
|
+
# This script generates trusted certificates for *.pan.localhost
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
CERTS_DIR="${SCRIPT_DIR}/../templates/traefik/certs"
|
|
9
|
+
|
|
10
|
+
# Colors for output
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
NC='\033[0m' # No Color
|
|
15
|
+
|
|
16
|
+
echo "Setting up local HTTPS certificates for Panopticon..."
|
|
17
|
+
|
|
18
|
+
# Check if mkcert is installed
|
|
19
|
+
if ! command -v mkcert &> /dev/null; then
|
|
20
|
+
echo -e "${RED}Error: mkcert is not installed.${NC}"
|
|
21
|
+
echo ""
|
|
22
|
+
echo "Install mkcert first:"
|
|
23
|
+
echo " macOS: brew install mkcert"
|
|
24
|
+
echo " Linux: apt install mkcert OR brew install mkcert"
|
|
25
|
+
echo " Windows: choco install mkcert"
|
|
26
|
+
echo ""
|
|
27
|
+
echo "More info: https://github.com/FiloSottile/mkcert"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo -e "${GREEN}mkcert found at $(which mkcert)${NC}"
|
|
32
|
+
|
|
33
|
+
# Check if CA is installed
|
|
34
|
+
CAROOT=$(mkcert -CAROOT 2>/dev/null)
|
|
35
|
+
if [ -f "$CAROOT/rootCA.pem" ]; then
|
|
36
|
+
echo -e "${GREEN}Local CA already exists at $CAROOT${NC}"
|
|
37
|
+
else
|
|
38
|
+
echo -e "${YELLOW}Installing local CA...${NC}"
|
|
39
|
+
echo "This may require sudo password to add CA to system trust store."
|
|
40
|
+
mkcert -install
|
|
41
|
+
echo -e "${GREEN}CA installed successfully${NC}"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Create certs directory if it doesn't exist
|
|
45
|
+
mkdir -p "$CERTS_DIR"
|
|
46
|
+
|
|
47
|
+
# Generate certificates for pan.localhost
|
|
48
|
+
echo ""
|
|
49
|
+
echo "Generating certificates for *.pan.localhost..."
|
|
50
|
+
cd "$CERTS_DIR"
|
|
51
|
+
|
|
52
|
+
# Generate wildcard cert for pan.localhost
|
|
53
|
+
mkcert -cert-file "_wildcard.pan.localhost.pem" \
|
|
54
|
+
-key-file "_wildcard.pan.localhost-key.pem" \
|
|
55
|
+
"pan.localhost" "*.pan.localhost"
|
|
56
|
+
|
|
57
|
+
echo ""
|
|
58
|
+
echo -e "${GREEN}Certificates generated successfully!${NC}"
|
|
59
|
+
echo ""
|
|
60
|
+
echo "Certificate files:"
|
|
61
|
+
ls -la "$CERTS_DIR"/*.pem
|
|
62
|
+
echo ""
|
|
63
|
+
echo "You can now start Traefik:"
|
|
64
|
+
echo " cd templates/traefik && docker compose up -d"
|
|
65
|
+
echo ""
|
|
66
|
+
echo "Access the dashboard at: https://pan.localhost"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/specialist-stop-hook
|
|
3
|
+
# Called when a specialist agent goes idle - detects completion patterns
|
|
4
|
+
#
|
|
5
|
+
# This hook is chained from stop-hook and only runs for specialist agents.
|
|
6
|
+
# It captures the last 50 lines of terminal output and looks for completion
|
|
7
|
+
# patterns to auto-trigger status updates.
|
|
8
|
+
|
|
9
|
+
# Don't use set -e - we want the hook to be resilient to failures
|
|
10
|
+
# and never break Claude Code execution
|
|
11
|
+
|
|
12
|
+
# Get agent ID from env or tmux session name
|
|
13
|
+
if [ -n "$PANOPTICON_AGENT_ID" ]; then
|
|
14
|
+
AGENT_ID="$PANOPTICON_AGENT_ID"
|
|
15
|
+
elif [ -n "$TMUX" ]; then
|
|
16
|
+
AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
|
|
17
|
+
else
|
|
18
|
+
exit 0 # Not in a tracked session
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Check if this is a specialist agent
|
|
22
|
+
case "$AGENT_ID" in
|
|
23
|
+
specialist-review-agent|specialist-test-agent|specialist-merge-agent)
|
|
24
|
+
;;
|
|
25
|
+
*)
|
|
26
|
+
exit 0 # Not a specialist, skip
|
|
27
|
+
;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
# Get the specialist type from agent ID
|
|
31
|
+
SPECIALIST_TYPE="${AGENT_ID#specialist-}"
|
|
32
|
+
|
|
33
|
+
# Capture the last 50 lines of terminal output
|
|
34
|
+
OUTPUT=$(tmux capture-pane -t "$AGENT_ID" -p -S -50 2>/dev/null || echo "")
|
|
35
|
+
|
|
36
|
+
if [ -z "$OUTPUT" ]; then
|
|
37
|
+
exit 0 # No output to analyze
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Define completion patterns for each specialist (case-insensitive grep -E patterns)
|
|
41
|
+
case "$SPECIALIST_TYPE" in
|
|
42
|
+
review-agent)
|
|
43
|
+
# Patterns that indicate review passed
|
|
44
|
+
PASS_PATTERN="[Rr]eview (passed|complete|PASSED|COMPLETE)|LGTM|[Nn]o issues found|[Cc]ode review complete|✓.*[Pp]assed"
|
|
45
|
+
# Patterns that indicate review failed/blocked
|
|
46
|
+
FAIL_PATTERN="[Rr]eview (failed|blocked|FAILED|BLOCKED)|[Ii]ssues found|[Nn]eeds changes|[Bb]locked"
|
|
47
|
+
STATUS_FIELD="reviewStatus"
|
|
48
|
+
;;
|
|
49
|
+
test-agent)
|
|
50
|
+
# Patterns that indicate tests passed
|
|
51
|
+
PASS_PATTERN="(all )?[Tt]ests? passed|test suite passed|0 failed|✓.*[Pp]assed|Tests PASSED"
|
|
52
|
+
# Patterns that indicate tests failed
|
|
53
|
+
FAIL_PATTERN="[Tt]ests? failed|test failures|[0-9]+ failed|✗.*[Ff]ailed|Tests FAILED"
|
|
54
|
+
STATUS_FIELD="testStatus"
|
|
55
|
+
;;
|
|
56
|
+
merge-agent)
|
|
57
|
+
# Patterns that indicate merge complete
|
|
58
|
+
PASS_PATTERN="[Mm]erge (complete|successful)|[Pp]ushed to|[Mm]erged to main|[Mm]erged successfully"
|
|
59
|
+
# Patterns that indicate merge failed
|
|
60
|
+
FAIL_PATTERN="[Mm]erge conflict|[Cc]annot merge|[Mm]erge failed"
|
|
61
|
+
STATUS_FIELD="mergeStatus"
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
exit 0
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
# Check for completion patterns (look at output)
|
|
69
|
+
STATUS=""
|
|
70
|
+
if echo "$OUTPUT" | grep -qE "$PASS_PATTERN"; then
|
|
71
|
+
STATUS="passed"
|
|
72
|
+
elif echo "$OUTPUT" | grep -qE "$FAIL_PATTERN"; then
|
|
73
|
+
STATUS="failed"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [ -z "$STATUS" ]; then
|
|
77
|
+
exit 0 # No recognized completion pattern - specialist is just idle
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Get current issue from specialist state file
|
|
81
|
+
STATE_FILE="$HOME/.panopticon/agents/$AGENT_ID/state.json"
|
|
82
|
+
ISSUE_ID=""
|
|
83
|
+
|
|
84
|
+
if [ -f "$STATE_FILE" ] && command -v jq &> /dev/null; then
|
|
85
|
+
ISSUE_ID=$(jq -r '.currentIssue // ""' "$STATE_FILE" 2>/dev/null || echo "")
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
if [ -z "$ISSUE_ID" ] || [ "$ISSUE_ID" = "null" ]; then
|
|
89
|
+
# No issue ID - cannot report completion
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Report completion to dashboard API (non-blocking background process)
|
|
94
|
+
# Only if dashboard is running
|
|
95
|
+
if curl -s -f --max-time 0.5 "http://localhost:3011/health" > /dev/null 2>&1; then
|
|
96
|
+
curl -s -X POST "http://localhost:3011/api/specialists/$SPECIALIST_TYPE/auto-complete" \
|
|
97
|
+
-H "Content-Type: application/json" \
|
|
98
|
+
-d "{\"issueId\":\"$ISSUE_ID\",\"status\":\"$STATUS\"}" \
|
|
99
|
+
> /dev/null 2>&1 &
|
|
100
|
+
|
|
101
|
+
# Log the completion detection
|
|
102
|
+
echo "[specialist-stop-hook] Detected completion: $SPECIALIST_TYPE -> $ISSUE_ID -> $STATUS" \
|
|
103
|
+
>> "$HOME/.panopticon/logs/hooks.log" 2>/dev/null || true
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
exit 0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/stop-hook
|
|
3
|
+
# Called by Claude Code when it finishes responding (waiting for input)
|
|
4
|
+
#
|
|
5
|
+
# This hook sets agent state to "idle" for auto-suspend detection
|
|
6
|
+
|
|
7
|
+
# Don't use set -e - we want the hook to be resilient to failures
|
|
8
|
+
# and never break Claude Code execution
|
|
9
|
+
|
|
10
|
+
# Get agent ID from env (set by pan work issue) or tmux session name
|
|
11
|
+
if [ -n "$PANOPTICON_AGENT_ID" ]; then
|
|
12
|
+
AGENT_ID="$PANOPTICON_AGENT_ID"
|
|
13
|
+
elif [ -n "$TMUX" ]; then
|
|
14
|
+
AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
|
|
15
|
+
else
|
|
16
|
+
AGENT_ID="main-cli"
|
|
17
|
+
fi
|
|
18
|
+
AGENT_ID="${AGENT_ID:-unknown}"
|
|
19
|
+
|
|
20
|
+
# Check if jq is available
|
|
21
|
+
if ! command -v jq &> /dev/null; then
|
|
22
|
+
exit 0 # Silent failure - don't break Claude Code execution
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Ensure state directory exists
|
|
26
|
+
STATE_DIR="$HOME/.panopticon/agents/$AGENT_ID"
|
|
27
|
+
mkdir -p "$STATE_DIR"
|
|
28
|
+
|
|
29
|
+
# Write state to file (atomic write via temp file)
|
|
30
|
+
TEMP_FILE="$STATE_DIR/state.json.tmp"
|
|
31
|
+
jq -n \
|
|
32
|
+
--arg timestamp "$(date -Iseconds)" \
|
|
33
|
+
--arg state "idle" \
|
|
34
|
+
'{
|
|
35
|
+
state: $state,
|
|
36
|
+
lastActivity: $timestamp
|
|
37
|
+
}' > "$TEMP_FILE" 2>/dev/null || true
|
|
38
|
+
|
|
39
|
+
mv "$TEMP_FILE" "$STATE_DIR/state.json" 2>/dev/null || true
|
|
40
|
+
|
|
41
|
+
# Optionally send heartbeat to API server (non-blocking)
|
|
42
|
+
# Only if dashboard is running
|
|
43
|
+
if curl -s -f --max-time 0.5 "http://localhost:3011/health" > /dev/null 2>&1; then
|
|
44
|
+
curl -s -X POST "http://localhost:3011/api/agents/$AGENT_ID/heartbeat" \
|
|
45
|
+
-H "Content-Type: application/json" \
|
|
46
|
+
-d "{\"state\":\"idle\",\"timestamp\":\"$(date -Iseconds)\"}" \
|
|
47
|
+
> /dev/null 2>&1 &
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Chain to specialist-specific hook for auto-completion detection
|
|
51
|
+
# This runs in background and only affects specialist agents
|
|
52
|
+
SPECIALIST_HOOK="$HOME/.panopticon/bin/specialist-stop-hook"
|
|
53
|
+
if [ -x "$SPECIALIST_HOOK" ]; then
|
|
54
|
+
"$SPECIALIST_HOOK" &
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Always exit successfully
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# validate-merge.sh - Validation script for merge completeness
|
|
3
|
+
# Checks for conflict markers, runs build, and runs tests
|
|
4
|
+
# Usage: validate-merge.sh [PROJECT_ROOT]
|
|
5
|
+
# Exit codes: 0 = validation passed, 1 = validation failed
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
PROJECT_ROOT="${1:-.}"
|
|
10
|
+
cd "$PROJECT_ROOT"
|
|
11
|
+
|
|
12
|
+
echo "=== Merge Validation ==="
|
|
13
|
+
echo "Project root: $(pwd)"
|
|
14
|
+
echo ""
|
|
15
|
+
|
|
16
|
+
# 1. Check for conflict markers
|
|
17
|
+
echo "Checking for conflict markers..."
|
|
18
|
+
CONFLICT_FOUND=false
|
|
19
|
+
|
|
20
|
+
# Check for <<<<<<< markers (at start of line only)
|
|
21
|
+
if git grep -l '^<<<<<<< ' 2>/dev/null; then
|
|
22
|
+
echo "ERROR: Conflict start markers found in files:"
|
|
23
|
+
git grep -l '^<<<<<<< '
|
|
24
|
+
CONFLICT_FOUND=true
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Check for ======= markers (at start of line)
|
|
28
|
+
if git grep -l '^=======$' 2>/dev/null; then
|
|
29
|
+
echo "ERROR: Conflict separator markers found in files:"
|
|
30
|
+
git grep -l '^=======$'
|
|
31
|
+
CONFLICT_FOUND=true
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Check for >>>>>>> markers (at start of line only)
|
|
35
|
+
if git grep -l '^>>>>>>> ' 2>/dev/null; then
|
|
36
|
+
echo "ERROR: Conflict end markers found in files:"
|
|
37
|
+
git grep -l '^>>>>>>> '
|
|
38
|
+
CONFLICT_FOUND=true
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if [ "$CONFLICT_FOUND" = true ]; then
|
|
42
|
+
echo ""
|
|
43
|
+
echo "VALIDATION FAILED: Conflict markers detected"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "✓ No conflict markers found"
|
|
48
|
+
echo ""
|
|
49
|
+
|
|
50
|
+
# 2. Run build
|
|
51
|
+
echo "Running build..."
|
|
52
|
+
if [ -f "package.json" ]; then
|
|
53
|
+
if npm run build 2>&1; then
|
|
54
|
+
echo "✓ Build passed"
|
|
55
|
+
else
|
|
56
|
+
echo ""
|
|
57
|
+
echo "VALIDATION FAILED: Build errors detected"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
elif [ -f "pom.xml" ]; then
|
|
61
|
+
if mvn compile 2>&1; then
|
|
62
|
+
echo "✓ Build passed"
|
|
63
|
+
else
|
|
64
|
+
echo ""
|
|
65
|
+
echo "VALIDATION FAILED: Build errors detected"
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
else
|
|
69
|
+
echo "⚠ No build system detected (no package.json or pom.xml), skipping build check"
|
|
70
|
+
fi
|
|
71
|
+
echo ""
|
|
72
|
+
|
|
73
|
+
# 3. Run tests
|
|
74
|
+
# BASELINE_FAILURES env var enables baseline comparison mode:
|
|
75
|
+
# If set, only fail when NEW failures appear (failures > baseline)
|
|
76
|
+
echo "Running tests..."
|
|
77
|
+
if [ -n "$BASELINE_FAILURES" ]; then
|
|
78
|
+
echo "Baseline comparison mode: $BASELINE_FAILURES pre-existing failures"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
if [ -f "package.json" ]; then
|
|
82
|
+
TEST_OUTPUT=$(npm test 2>&1) || true
|
|
83
|
+
echo "$TEST_OUTPUT"
|
|
84
|
+
|
|
85
|
+
# Check if tests passed cleanly
|
|
86
|
+
if echo "$TEST_OUTPUT" | grep -q "Tests.*passed\|0 failed"; then
|
|
87
|
+
echo "✓ Tests passed"
|
|
88
|
+
elif [ -n "$BASELINE_FAILURES" ]; then
|
|
89
|
+
# Baseline mode: extract failure count and compare
|
|
90
|
+
# Match vitest output format: "X failed" or "X tests failed"
|
|
91
|
+
ACTUAL_FAILURES=$(echo "$TEST_OUTPUT" | grep -oP '(\d+)\s+failed' | head -1 | grep -oP '\d+' || echo "0")
|
|
92
|
+
if [ "$ACTUAL_FAILURES" -le "$BASELINE_FAILURES" ]; then
|
|
93
|
+
echo ""
|
|
94
|
+
echo "✓ Tests passed (baseline comparison: $ACTUAL_FAILURES failures <= $BASELINE_FAILURES baseline)"
|
|
95
|
+
else
|
|
96
|
+
echo ""
|
|
97
|
+
echo "VALIDATION FAILED: New test failures detected ($ACTUAL_FAILURES > baseline $BASELINE_FAILURES)"
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
else
|
|
101
|
+
echo ""
|
|
102
|
+
echo "VALIDATION FAILED: Test failures detected"
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
elif [ -f "pom.xml" ]; then
|
|
106
|
+
if mvn test 2>&1; then
|
|
107
|
+
echo "✓ Tests passed"
|
|
108
|
+
else
|
|
109
|
+
echo ""
|
|
110
|
+
echo "VALIDATION FAILED: Test failures detected"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
else
|
|
114
|
+
echo "⚠ No test system detected, skipping test check"
|
|
115
|
+
fi
|
|
116
|
+
echo ""
|
|
117
|
+
|
|
118
|
+
echo "=== VALIDATION PASSED ==="
|
|
119
|
+
exit 0
|