panopticon-cli 0.4.32 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -210
- package/dist/{agents-BDFHF4T3.js → agents-E43Y3HNU.js} +10 -7
- package/dist/chunk-7SN4L4PH.js +150 -0
- package/dist/chunk-7SN4L4PH.js.map +1 -0
- package/dist/{chunk-2NIAOCIC.js → chunk-AAFQANKW.js} +358 -97
- package/dist/chunk-AAFQANKW.js.map +1 -0
- package/dist/chunk-AQXETQHW.js +113 -0
- package/dist/chunk-AQXETQHW.js.map +1 -0
- package/dist/chunk-B3PF6JPQ.js +212 -0
- package/dist/chunk-B3PF6JPQ.js.map +1 -0
- package/dist/chunk-CFCUOV3Q.js +669 -0
- package/dist/chunk-CFCUOV3Q.js.map +1 -0
- package/dist/chunk-CWELWPWQ.js +32 -0
- package/dist/chunk-CWELWPWQ.js.map +1 -0
- package/dist/chunk-DI7ABPNQ.js +352 -0
- package/dist/chunk-DI7ABPNQ.js.map +1 -0
- package/dist/{chunk-VU4FLXV5.js → chunk-FQ66DECN.js} +31 -4
- package/dist/chunk-FQ66DECN.js.map +1 -0
- package/dist/{chunk-VIWUCJ4V.js → chunk-FTCPTHIJ.js} +57 -432
- package/dist/chunk-FTCPTHIJ.js.map +1 -0
- package/dist/{review-status-GWQYY77L.js → chunk-GFP3PIPB.js} +14 -7
- package/dist/chunk-GFP3PIPB.js.map +1 -0
- package/dist/chunk-GR6ZZMCX.js +816 -0
- package/dist/chunk-GR6ZZMCX.js.map +1 -0
- package/dist/chunk-HJSM6E6U.js +1038 -0
- package/dist/chunk-HJSM6E6U.js.map +1 -0
- package/dist/{chunk-XP2DXWYP.js → chunk-HZT2AOPN.js} +164 -39
- package/dist/chunk-HZT2AOPN.js.map +1 -0
- package/dist/chunk-JQBV3Q2W.js +29 -0
- package/dist/chunk-JQBV3Q2W.js.map +1 -0
- package/dist/{chunk-BWGFN44T.js → chunk-JT4O4YVM.js} +28 -16
- package/dist/chunk-JT4O4YVM.js.map +1 -0
- package/dist/chunk-NTO3EDB3.js +600 -0
- package/dist/chunk-NTO3EDB3.js.map +1 -0
- package/dist/{chunk-JY7R7V4G.js → chunk-OMNXYPXC.js} +2 -2
- package/dist/chunk-OMNXYPXC.js.map +1 -0
- package/dist/chunk-PELXV435.js +215 -0
- package/dist/chunk-PELXV435.js.map +1 -0
- package/dist/chunk-PPRFKTVC.js +154 -0
- package/dist/chunk-PPRFKTVC.js.map +1 -0
- package/dist/chunk-WQG2TYCB.js +677 -0
- package/dist/chunk-WQG2TYCB.js.map +1 -0
- package/dist/{chunk-HCTJFIJJ.js → chunk-YLPSQAM2.js} +2 -2
- package/dist/{chunk-HCTJFIJJ.js.map → chunk-YLPSQAM2.js.map} +1 -1
- package/dist/{chunk-6HXKTOD7.js → chunk-ZTFNYOC7.js} +53 -38
- package/dist/chunk-ZTFNYOC7.js.map +1 -0
- package/dist/cli/index.js +5103 -3165
- package/dist/cli/index.js.map +1 -1
- package/dist/{config-BOAMSKTF.js → config-4CJNUE3O.js} +7 -3
- package/dist/dashboard/prompts/merge-agent.md +217 -0
- package/dist/dashboard/prompts/review-agent.md +409 -0
- package/dist/dashboard/prompts/sync-main.md +84 -0
- package/dist/dashboard/prompts/test-agent.md +283 -0
- package/dist/dashboard/prompts/work-agent.md +249 -0
- package/dist/dashboard/public/assets/index-BxpjweAL.css +32 -0
- package/dist/dashboard/public/assets/index-DQHkwvvJ.js +743 -0
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/server.js +17619 -4044
- package/dist/{dns-L3L2BB27.js → dns-7BDJSD3E.js} +4 -2
- package/dist/{feedback-writer-AAKF5BTK.js → feedback-writer-LVZ5TFYZ.js} +8 -4
- package/dist/feedback-writer-LVZ5TFYZ.js.map +1 -0
- package/dist/hume-WMAUBBV2.js +13 -0
- package/dist/index.d.ts +162 -40
- package/dist/index.js +67 -23
- package/dist/index.js.map +1 -1
- package/dist/{projects-VXRUCMLM.js → projects-JEIVIYC6.js} +3 -3
- package/dist/rally-RKFSWC7E.js +10 -0
- package/dist/{remote-agents-Z3R2A5BN.js → remote-agents-TFSMW7GN.js} +2 -2
- package/dist/{remote-workspace-2G6V2KNP.js → remote-workspace-AHVHQEES.js} +8 -8
- package/dist/review-status-EPFG4XM7.js +19 -0
- package/dist/shadow-state-5MDP6YXH.js +30 -0
- package/dist/shadow-state-5MDP6YXH.js.map +1 -0
- package/dist/{specialist-context-N32QBNNQ.js → specialist-context-ZC6A4M3I.js} +8 -7
- package/dist/{specialist-context-N32QBNNQ.js.map → specialist-context-ZC6A4M3I.js.map} +1 -1
- package/dist/{specialist-logs-GF3YV4KL.js → specialist-logs-KLGJCEUL.js} +7 -6
- package/dist/specialist-logs-KLGJCEUL.js.map +1 -0
- package/dist/{specialists-JBIW6MP4.js → specialists-O4HWDJL5.js} +7 -6
- package/dist/specialists-O4HWDJL5.js.map +1 -0
- package/dist/tldr-daemon-T3THOUGT.js +21 -0
- package/dist/tldr-daemon-T3THOUGT.js.map +1 -0
- package/dist/traefik-QN7R5I6V.js +19 -0
- package/dist/traefik-QN7R5I6V.js.map +1 -0
- package/dist/tunnel-W2GZBLEV.js +13 -0
- package/dist/tunnel-W2GZBLEV.js.map +1 -0
- package/dist/workspace-manager-IE4JL2JP.js +22 -0
- package/dist/workspace-manager-IE4JL2JP.js.map +1 -0
- package/package.json +2 -2
- package/scripts/heartbeat-hook +37 -10
- package/scripts/patches/llm-tldr-tsx-support.py +109 -0
- package/scripts/pre-tool-hook +26 -15
- package/scripts/record-cost-event.js +177 -43
- package/scripts/record-cost-event.ts +87 -3
- package/scripts/statusline.sh +169 -0
- package/scripts/stop-hook +21 -11
- package/scripts/tldr-post-edit +72 -0
- package/scripts/tldr-read-enforcer +275 -0
- package/scripts/work-agent-stop-hook +137 -0
- package/skills/check-merged/SKILL.md +143 -0
- package/skills/crash-investigation/SKILL.md +301 -0
- package/skills/github-cli/SKILL.md +185 -0
- package/skills/myn-standards/SKILL.md +351 -0
- package/skills/pan-reopen/SKILL.md +65 -0
- package/skills/pan-sync-main/SKILL.md +87 -0
- package/skills/pan-tldr/SKILL.md +149 -0
- package/skills/react-best-practices/SKILL.md +125 -0
- package/skills/spec-readiness/REPORT-TEMPLATE.md +158 -0
- package/skills/spec-readiness/SCORING-REFERENCE.md +369 -0
- package/skills/spec-readiness/SKILL.md +400 -0
- package/skills/spec-readiness-setup/SKILL.md +361 -0
- package/skills/workspace-status/SKILL.md +56 -0
- package/skills/write-spec/SKILL.md +138 -0
- package/templates/traefik/dynamic/panopticon.yml.template +0 -5
- package/templates/traefik/traefik.yml +0 -8
- package/dist/chunk-2NIAOCIC.js.map +0 -1
- package/dist/chunk-3XAB4IXF.js +0 -51
- package/dist/chunk-3XAB4IXF.js.map +0 -1
- package/dist/chunk-6HXKTOD7.js.map +0 -1
- package/dist/chunk-BBCUK6N2.js +0 -241
- package/dist/chunk-BBCUK6N2.js.map +0 -1
- package/dist/chunk-BWGFN44T.js.map +0 -1
- package/dist/chunk-ELK6Q7QI.js +0 -545
- package/dist/chunk-ELK6Q7QI.js.map +0 -1
- package/dist/chunk-JY7R7V4G.js.map +0 -1
- package/dist/chunk-LYSBSZYV.js +0 -1523
- package/dist/chunk-LYSBSZYV.js.map +0 -1
- package/dist/chunk-VIWUCJ4V.js.map +0 -1
- package/dist/chunk-VU4FLXV5.js.map +0 -1
- package/dist/chunk-XP2DXWYP.js.map +0 -1
- package/dist/dashboard/public/assets/index-C7X6LP5Z.css +0 -32
- package/dist/dashboard/public/assets/index-ClYqpcAJ.js +0 -645
- package/dist/feedback-writer-AAKF5BTK.js.map +0 -1
- package/dist/review-status-GWQYY77L.js.map +0 -1
- package/dist/traefik-CUJM6K5Z.js +0 -12
- /package/dist/{agents-BDFHF4T3.js.map → agents-E43Y3HNU.js.map} +0 -0
- /package/dist/{config-BOAMSKTF.js.map → config-4CJNUE3O.js.map} +0 -0
- /package/dist/{dns-L3L2BB27.js.map → dns-7BDJSD3E.js.map} +0 -0
- /package/dist/{projects-VXRUCMLM.js.map → hume-WMAUBBV2.js.map} +0 -0
- /package/dist/{remote-agents-Z3R2A5BN.js.map → projects-JEIVIYC6.js.map} +0 -0
- /package/dist/{specialist-logs-GF3YV4KL.js.map → rally-RKFSWC7E.js.map} +0 -0
- /package/dist/{specialists-JBIW6MP4.js.map → remote-agents-TFSMW7GN.js.map} +0 -0
- /package/dist/{remote-workspace-2G6V2KNP.js.map → remote-workspace-AHVHQEES.js.map} +0 -0
- /package/dist/{traefik-CUJM6K5Z.js.map → review-status-EPFG4XM7.js.map} +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/tldr-post-edit
|
|
3
|
+
# PostToolUse hook on Edit/Write — notifies TLDR to re-warm after code changes.
|
|
4
|
+
#
|
|
5
|
+
# Tracks dirty files in a lightweight file. When the dirty count exceeds a
|
|
6
|
+
# threshold, triggers a background re-warm so the index stays fresh.
|
|
7
|
+
|
|
8
|
+
# Don't use set -e — never break Claude Code execution
|
|
9
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
10
|
+
|
|
11
|
+
# Only act on Edit and Write tools
|
|
12
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
13
|
+
if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Extract file path
|
|
18
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
19
|
+
if [ -z "$FILE_PATH" ]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Only track code files
|
|
24
|
+
EXT="${FILE_PATH##*.}"
|
|
25
|
+
EXT_LOWER=$(echo "$EXT" | tr '[:upper:]' '[:lower:]')
|
|
26
|
+
case "$EXT_LOWER" in
|
|
27
|
+
ts|tsx|js|jsx|py|java|go|rs|cpp|c|h|hpp|rb|php|kt|swift|cs|scala|lua|ex|exs)
|
|
28
|
+
;;
|
|
29
|
+
*)
|
|
30
|
+
exit 0
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
# Find project root (where .venv lives)
|
|
35
|
+
PROJECT_DIR=""
|
|
36
|
+
DIR="$(dirname "$FILE_PATH")"
|
|
37
|
+
while [ "$DIR" != "/" ]; do
|
|
38
|
+
if [ -d "$DIR/.venv" ]; then
|
|
39
|
+
PROJECT_DIR="$DIR"
|
|
40
|
+
break
|
|
41
|
+
fi
|
|
42
|
+
DIR=$(dirname "$DIR")
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if [ -z "$PROJECT_DIR" ]; then
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Track dirty files
|
|
50
|
+
DIRTY_FILE="$PROJECT_DIR/.tldr/dirty-files"
|
|
51
|
+
mkdir -p "$PROJECT_DIR/.tldr" 2>/dev/null || true
|
|
52
|
+
|
|
53
|
+
# Append the changed file (deduplicated on warm)
|
|
54
|
+
REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
|
|
55
|
+
echo "$REL_PATH" >> "$DIRTY_FILE" 2>/dev/null || true
|
|
56
|
+
|
|
57
|
+
# Count dirty files (fast line count)
|
|
58
|
+
DIRTY_COUNT=$(wc -l < "$DIRTY_FILE" 2>/dev/null || echo 0)
|
|
59
|
+
|
|
60
|
+
# Trigger background re-warm after 10 edits (keeps index reasonably fresh)
|
|
61
|
+
WARM_THRESHOLD=10
|
|
62
|
+
if [ "$DIRTY_COUNT" -ge "$WARM_THRESHOLD" ]; then
|
|
63
|
+
TLDR_BIN="$PROJECT_DIR/.venv/bin/tldr"
|
|
64
|
+
if [ -x "$TLDR_BIN" ]; then
|
|
65
|
+
# Clear dirty tracking before warm
|
|
66
|
+
: > "$DIRTY_FILE" 2>/dev/null || true
|
|
67
|
+
# Background warm — non-blocking
|
|
68
|
+
(cd "$PROJECT_DIR" && "$TLDR_BIN" warm . --background) >/dev/null 2>&1 &
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
exit 0
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/tldr-read-enforcer
|
|
3
|
+
# PreToolUse hook on Read — intercepts file reads and returns TLDR summaries
|
|
4
|
+
# for large code files, saving 90-95% of context tokens.
|
|
5
|
+
#
|
|
6
|
+
# Bypasses (allows normal read):
|
|
7
|
+
# - Files < 3KB (small enough to read directly)
|
|
8
|
+
# - Reads with offset/limit (targeted reads for editing)
|
|
9
|
+
# - Non-code files (configs, docs, json, etc.)
|
|
10
|
+
# - No .venv available (TLDR not installed)
|
|
11
|
+
# - TLDR command fails (graceful degradation)
|
|
12
|
+
# - Summary too sparse to be useful (< 100 tokens for file > 5KB)
|
|
13
|
+
# - Recently edited files (in .tldr/dirty-files — agent needs to verify changes)
|
|
14
|
+
|
|
15
|
+
# Don't use set -e — never break Claude Code execution
|
|
16
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
17
|
+
|
|
18
|
+
# Only act on Read tool
|
|
19
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
|
|
20
|
+
if [ "$TOOL_NAME" != "Read" ]; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Extract Read parameters
|
|
25
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null)
|
|
26
|
+
OFFSET=$(echo "$INPUT" | jq -r '.tool_input.offset // empty' 2>/dev/null)
|
|
27
|
+
LIMIT=$(echo "$INPUT" | jq -r '.tool_input.limit // empty' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
# Bypass: targeted reads (offset or limit specified — agent is reading for editing)
|
|
30
|
+
if [ -n "$OFFSET" ] || [ -n "$LIMIT" ]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Bypass: file doesn't exist
|
|
35
|
+
if [ ! -f "$FILE_PATH" ]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Bypass: small files (< 3KB)
|
|
40
|
+
FILE_SIZE=$(stat -c%s "$FILE_PATH" 2>/dev/null || echo 0)
|
|
41
|
+
if [ "$FILE_SIZE" -lt 3072 ]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Bypass: non-code files
|
|
46
|
+
EXT="${FILE_PATH##*.}"
|
|
47
|
+
EXT_LOWER=$(echo "$EXT" | tr '[:upper:]' '[:lower:]')
|
|
48
|
+
case "$EXT_LOWER" in
|
|
49
|
+
ts|tsx|js|jsx|py|java|go|rs|cpp|c|h|hpp|rb|php|kt|swift|cs|scala|lua|ex|exs)
|
|
50
|
+
# Code file — continue to TLDR
|
|
51
|
+
;;
|
|
52
|
+
*)
|
|
53
|
+
# Non-code — allow normal read
|
|
54
|
+
exit 0
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
# Find the .venv/bin/tldr binary
|
|
59
|
+
# Check workspace first, then project root
|
|
60
|
+
TLDR_BIN=""
|
|
61
|
+
DIR="$(dirname "$FILE_PATH")"
|
|
62
|
+
while [ "$DIR" != "/" ]; do
|
|
63
|
+
if [ -x "$DIR/.venv/bin/tldr" ]; then
|
|
64
|
+
TLDR_BIN="$DIR/.venv/bin/tldr"
|
|
65
|
+
PROJECT_DIR="$DIR"
|
|
66
|
+
break
|
|
67
|
+
fi
|
|
68
|
+
DIR=$(dirname "$DIR")
|
|
69
|
+
done
|
|
70
|
+
|
|
71
|
+
# Bypass: no TLDR binary found
|
|
72
|
+
if [ -z "$TLDR_BIN" ]; then
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Get relative path from project root
|
|
77
|
+
REL_PATH="${FILE_PATH#$PROJECT_DIR/}"
|
|
78
|
+
|
|
79
|
+
# Bypass: recently edited files (agent needs to verify its own changes)
|
|
80
|
+
# The post-edit hook tracks edits in .tldr/dirty-files
|
|
81
|
+
DIRTY_FILE="$PROJECT_DIR/.tldr/dirty-files"
|
|
82
|
+
if [ -f "$DIRTY_FILE" ] && grep -qxF "$REL_PATH" "$DIRTY_FILE" 2>/dev/null; then
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Detect language from extension for tldr --lang flag
|
|
87
|
+
case "$EXT_LOWER" in
|
|
88
|
+
ts|tsx) TLDR_LANG="typescript" ;;
|
|
89
|
+
js|jsx) TLDR_LANG="javascript" ;;
|
|
90
|
+
py) TLDR_LANG="python" ;;
|
|
91
|
+
go) TLDR_LANG="go" ;;
|
|
92
|
+
rs) TLDR_LANG="rust" ;;
|
|
93
|
+
java|kt) TLDR_LANG="java" ;;
|
|
94
|
+
rb) TLDR_LANG="ruby" ;;
|
|
95
|
+
*) TLDR_LANG="python" ;;
|
|
96
|
+
esac
|
|
97
|
+
|
|
98
|
+
# Strip file extension — tldr context expects module paths without extension
|
|
99
|
+
# e.g., "src/lib/agents" not "src/lib/agents.ts"
|
|
100
|
+
MODULE_PATH="${REL_PATH%.*}"
|
|
101
|
+
|
|
102
|
+
# Try to get TLDR context for this file (module path mode)
|
|
103
|
+
TLDR_OUTPUT=$("$TLDR_BIN" context "$MODULE_PATH" --lang "$TLDR_LANG" 2>/dev/null)
|
|
104
|
+
TLDR_EXIT=$?
|
|
105
|
+
|
|
106
|
+
# Quality gate: check if context output is too sparse to be useful.
|
|
107
|
+
# Pattern: "~XX tokens" where XX < 100 means the summary captured almost nothing.
|
|
108
|
+
# This happens with test files (describe/it blocks) and type-only files.
|
|
109
|
+
CONTEXT_SPARSE=false
|
|
110
|
+
if [ $TLDR_EXIT -eq 0 ] && [ -n "$TLDR_OUTPUT" ]; then
|
|
111
|
+
CTX_TOKENS=$(echo "$TLDR_OUTPUT" | grep -oP '\~\K\d+(?= tokens)' || echo "0")
|
|
112
|
+
if [ "$CTX_TOKENS" -lt 100 ] && [ "$FILE_SIZE" -gt 5120 ]; then
|
|
113
|
+
CONTEXT_SPARSE=true
|
|
114
|
+
fi
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Fallback: if context failed, was sparse, or empty — try extract command
|
|
118
|
+
# extract works on actual file paths (including .tsx) and returns structured JSON
|
|
119
|
+
if [ $TLDR_EXIT -ne 0 ] || [ -z "$TLDR_OUTPUT" ] || [ "$CONTEXT_SPARSE" = true ]; then
|
|
120
|
+
EXTRACT_JSON=$("$TLDR_BIN" extract "$REL_PATH" 2>/dev/null)
|
|
121
|
+
EXTRACT_EXIT=$?
|
|
122
|
+
if [ $EXTRACT_EXIT -eq 0 ] && [ -n "$EXTRACT_JSON" ]; then
|
|
123
|
+
# Check if extract found any real content
|
|
124
|
+
EXTRACT_COUNTS=$(echo "$EXTRACT_JSON" | python3 -c "
|
|
125
|
+
import json, sys
|
|
126
|
+
try:
|
|
127
|
+
d = json.load(sys.stdin)
|
|
128
|
+
nf = len(d.get('functions', []))
|
|
129
|
+
nc = len(d.get('classes', []))
|
|
130
|
+
print(f'{nf} {nc}')
|
|
131
|
+
except:
|
|
132
|
+
print('0 0')
|
|
133
|
+
" 2>/dev/null)
|
|
134
|
+
EXTRACT_FUNCS=$(echo "$EXTRACT_COUNTS" | cut -d' ' -f1)
|
|
135
|
+
EXTRACT_CLASSES=$(echo "$EXTRACT_COUNTS" | cut -d' ' -f2)
|
|
136
|
+
|
|
137
|
+
# If extract also found nothing useful, bypass entirely
|
|
138
|
+
if [ "${EXTRACT_FUNCS:-0}" -eq 0 ] && [ "${EXTRACT_CLASSES:-0}" -eq 0 ]; then
|
|
139
|
+
# Neither context nor extract found useful content — let the agent read the file
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Convert extract JSON to a readable summary with language-appropriate syntax
|
|
144
|
+
TLDR_OUTPUT=$(echo "$EXTRACT_JSON" | python3 -c "
|
|
145
|
+
import json, sys
|
|
146
|
+
try:
|
|
147
|
+
data = json.load(sys.stdin)
|
|
148
|
+
lang = '$TLDR_LANG'
|
|
149
|
+
lines = []
|
|
150
|
+
fname = data.get('file_path', '$REL_PATH')
|
|
151
|
+
short = fname.split('/')[-1]
|
|
152
|
+
lines.append(f'## Code Context: {fname}')
|
|
153
|
+
lines.append('')
|
|
154
|
+
|
|
155
|
+
# Language-appropriate function keyword
|
|
156
|
+
fn_kw = {'typescript': 'function', 'javascript': 'function', 'python': 'def',
|
|
157
|
+
'go': 'func', 'rust': 'fn', 'java': '', 'ruby': 'def'}.get(lang, 'function')
|
|
158
|
+
|
|
159
|
+
for func in data.get('functions', []):
|
|
160
|
+
name = func.get('name', '?')
|
|
161
|
+
params = ', '.join(
|
|
162
|
+
p.get('name','') + (': ' + p.get('type','') if p.get('type') else '')
|
|
163
|
+
for p in func.get('parameters', [])
|
|
164
|
+
)
|
|
165
|
+
ret = func.get('return_type', '')
|
|
166
|
+
doc = (func.get('docstring') or '')[:80]
|
|
167
|
+
line = func.get('start_line') or '?'
|
|
168
|
+
sig = f'{fn_kw} {name}({params})'.strip()
|
|
169
|
+
if ret:
|
|
170
|
+
if lang in ('typescript', 'javascript', 'go', 'rust'):
|
|
171
|
+
sig += f': {ret}'
|
|
172
|
+
else:
|
|
173
|
+
sig += f' -> {ret}'
|
|
174
|
+
lines.append(f'{name} ({short}:{line})')
|
|
175
|
+
lines.append(f' {sig}')
|
|
176
|
+
if doc:
|
|
177
|
+
lines.append(f' // {doc}')
|
|
178
|
+
lines.append('')
|
|
179
|
+
|
|
180
|
+
for cls in data.get('classes', []):
|
|
181
|
+
name = cls.get('name', '?')
|
|
182
|
+
line = cls.get('start_line') or '?'
|
|
183
|
+
lines.append(f'class {name} ({short}:{line})')
|
|
184
|
+
for m in cls.get('methods', []):
|
|
185
|
+
mname = m.get('name', '?')
|
|
186
|
+
lines.append(f' .{mname}()')
|
|
187
|
+
lines.append('')
|
|
188
|
+
|
|
189
|
+
nfunc = len(data.get('functions', []))
|
|
190
|
+
ncls = len(data.get('classes', []))
|
|
191
|
+
lines.append(f'---')
|
|
192
|
+
lines.append(f'{nfunc} functions, {ncls} classes (via extract)')
|
|
193
|
+
print('\n'.join(lines))
|
|
194
|
+
except:
|
|
195
|
+
pass
|
|
196
|
+
" 2>/dev/null)
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# Bypass: both context and extract failed or produced nothing useful
|
|
201
|
+
if [ -z "$TLDR_OUTPUT" ]; then
|
|
202
|
+
exit 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# Also get the file's imports and format them
|
|
206
|
+
TLDR_IMPORTS_RAW=$("$TLDR_BIN" imports "$REL_PATH" --lang "$TLDR_LANG" 2>/dev/null || true)
|
|
207
|
+
TLDR_IMPORTS=""
|
|
208
|
+
if [ -n "$TLDR_IMPORTS_RAW" ]; then
|
|
209
|
+
TLDR_IMPORTS=$(echo "$TLDR_IMPORTS_RAW" | python3 -c "
|
|
210
|
+
import json, sys
|
|
211
|
+
try:
|
|
212
|
+
data = json.load(sys.stdin)
|
|
213
|
+
lines = []
|
|
214
|
+
for imp in data:
|
|
215
|
+
mod = imp.get('module', '?')
|
|
216
|
+
names = imp.get('names', [])
|
|
217
|
+
default = imp.get('default')
|
|
218
|
+
parts = []
|
|
219
|
+
if default:
|
|
220
|
+
parts.append(default)
|
|
221
|
+
if names:
|
|
222
|
+
parts.append('{ ' + ', '.join(names) + ' }')
|
|
223
|
+
if parts:
|
|
224
|
+
lines.append(f'import {', '.join(parts)} from \"{mod}\"')
|
|
225
|
+
else:
|
|
226
|
+
lines.append(f'import \"{mod}\"')
|
|
227
|
+
print('\n'.join(lines))
|
|
228
|
+
except:
|
|
229
|
+
# If JSON parsing fails, use raw output (may already be formatted)
|
|
230
|
+
sys.stdout.write(sys.stdin.read() if hasattr(sys, '_raw') else '')
|
|
231
|
+
" 2>/dev/null)
|
|
232
|
+
# If Python formatting failed, fall back to raw
|
|
233
|
+
if [ -z "$TLDR_IMPORTS" ]; then
|
|
234
|
+
TLDR_IMPORTS="$TLDR_IMPORTS_RAW"
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# Build the summary that Claude will see instead of the raw file
|
|
239
|
+
SUMMARY="[TLDR Summary — ${FILE_SIZE} bytes saved from context]
|
|
240
|
+
|
|
241
|
+
File: ${FILE_PATH}
|
|
242
|
+
|
|
243
|
+
## Structure & Exports
|
|
244
|
+
${TLDR_OUTPUT}"
|
|
245
|
+
|
|
246
|
+
if [ -n "$TLDR_IMPORTS" ]; then
|
|
247
|
+
SUMMARY="${SUMMARY}
|
|
248
|
+
|
|
249
|
+
## Imports
|
|
250
|
+
${TLDR_IMPORTS}"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
SUMMARY="${SUMMARY}
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
To read the full file, use Read with offset/limit parameters for the specific section you need to edit."
|
|
257
|
+
|
|
258
|
+
# Deny the read and provide TLDR context instead
|
|
259
|
+
# Use a temp file for the JSON to handle multiline safely
|
|
260
|
+
TEMP_JSON=$(mktemp /tmp/tldr-hook-XXXXXX.json)
|
|
261
|
+
jq -n --arg reason "TLDR summary provided instead of full file read (${FILE_SIZE} bytes → ~1K tokens)" \
|
|
262
|
+
--arg context "$SUMMARY" \
|
|
263
|
+
'{
|
|
264
|
+
hookSpecificOutput: {
|
|
265
|
+
hookEventName: "PreToolUse",
|
|
266
|
+
permissionDecision: "deny",
|
|
267
|
+
permissionDecisionReason: $reason,
|
|
268
|
+
additionalContext: $context
|
|
269
|
+
}
|
|
270
|
+
}' > "$TEMP_JSON" 2>/dev/null
|
|
271
|
+
|
|
272
|
+
cat "$TEMP_JSON"
|
|
273
|
+
rm -f "$TEMP_JSON"
|
|
274
|
+
|
|
275
|
+
exit 0
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/work-agent-stop-hook
|
|
3
|
+
# Called when any agent goes idle — detects work agents that forgot "pan work done"
|
|
4
|
+
#
|
|
5
|
+
# Uses a lightweight AI model to analyze the last N lines of terminal output
|
|
6
|
+
# and determine if the agent completed its work but failed to signal completion.
|
|
7
|
+
# If so, sends a nudge message to the agent via tmux.
|
|
8
|
+
#
|
|
9
|
+
# The model used is configurable via PANOPTICON_COMPLETION_CHECK_MODEL env var
|
|
10
|
+
# or defaults to the models.overrides.completion-check-hook setting in config.yaml,
|
|
11
|
+
# falling back to claude-haiku-4-5.
|
|
12
|
+
|
|
13
|
+
# Don't use set -e - resilient to failures, never break Claude Code execution
|
|
14
|
+
|
|
15
|
+
# Get agent ID
|
|
16
|
+
if [ -n "$PANOPTICON_AGENT_ID" ]; then
|
|
17
|
+
AGENT_ID="$PANOPTICON_AGENT_ID"
|
|
18
|
+
elif [ -n "$TMUX" ]; then
|
|
19
|
+
AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
|
|
20
|
+
else
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Only run for work agents (agent-min-XXX, agent-pan-XXX, etc.)
|
|
25
|
+
case "$AGENT_ID" in
|
|
26
|
+
agent-*)
|
|
27
|
+
# Check it's not a specialist
|
|
28
|
+
case "$AGENT_ID" in
|
|
29
|
+
specialist-*) exit 0 ;;
|
|
30
|
+
esac
|
|
31
|
+
;;
|
|
32
|
+
*)
|
|
33
|
+
exit 0 # Not a work agent
|
|
34
|
+
;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
37
|
+
# Extract issue ID from agent ID (e.g., "agent-min-725" -> "MIN-725")
|
|
38
|
+
ISSUE_ID=$(echo "$AGENT_ID" | sed 's/^agent-//' | tr '[:lower:]' '[:upper:]')
|
|
39
|
+
|
|
40
|
+
# Skip if completion marker already exists (agent already called pan work done)
|
|
41
|
+
COMPLETED_FILE="$HOME/.panopticon/agents/$AGENT_ID/completed"
|
|
42
|
+
if [ -f "$COMPLETED_FILE" ]; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Cooldown: don't nudge more than once per 10 minutes
|
|
47
|
+
NUDGE_FILE="$HOME/.panopticon/agents/$AGENT_ID/.last-completion-nudge"
|
|
48
|
+
if [ -f "$NUDGE_FILE" ]; then
|
|
49
|
+
LAST_NUDGE=$(cat "$NUDGE_FILE" 2>/dev/null || echo "0")
|
|
50
|
+
NOW=$(date +%s)
|
|
51
|
+
ELAPSED=$(( NOW - LAST_NUDGE ))
|
|
52
|
+
if [ "$ELAPSED" -lt 600 ]; then
|
|
53
|
+
exit 0 # Too soon since last nudge
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Capture the last 80 lines of terminal output
|
|
58
|
+
OUTPUT=$(tmux capture-pane -t "$AGENT_ID" -p -S -80 2>/dev/null || echo "")
|
|
59
|
+
if [ -z "$OUTPUT" ]; then
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Quick heuristic pre-check: skip the expensive AI call if the output clearly
|
|
64
|
+
# shows the agent is mid-work (e.g., running tests, editing files, reading)
|
|
65
|
+
if echo "$OUTPUT" | grep -qE '(Reading|Editing|Searching|Running|Compiling|Building|Installing|●.*Bash|●.*Read|●.*Edit|●.*Grep|●.*Write|●.*Glob)' | tail -5 | grep -qE '●'; then
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Check if the agent appears to be at an idle prompt (not mid-response)
|
|
70
|
+
if ! echo "$OUTPUT" | tail -10 | grep -qE '(^❯|Worked for)'; then
|
|
71
|
+
exit 0 # Agent doesn't appear to be at an idle prompt
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Determine model to use for completion check
|
|
75
|
+
# Priority: env var > config.yaml override > default
|
|
76
|
+
COMPLETION_MODEL="${PANOPTICON_COMPLETION_CHECK_MODEL:-}"
|
|
77
|
+
if [ -z "$COMPLETION_MODEL" ]; then
|
|
78
|
+
CONFIG_FILE="$HOME/.panopticon/config.yaml"
|
|
79
|
+
if [ -f "$CONFIG_FILE" ] && command -v grep &> /dev/null; then
|
|
80
|
+
COMPLETION_MODEL=$(grep 'completion-check-hook:' "$CONFIG_FILE" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "")
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
COMPLETION_MODEL="${COMPLETION_MODEL:-claude-haiku-4-5}"
|
|
84
|
+
|
|
85
|
+
# Build the analysis prompt
|
|
86
|
+
ANALYSIS_PROMPT="You are analyzing a work agent's terminal output to determine if it finished its work but forgot to call 'pan work done'.
|
|
87
|
+
|
|
88
|
+
The agent was working on issue $ISSUE_ID. Here is the last 80 lines of its terminal output:
|
|
89
|
+
|
|
90
|
+
<terminal_output>
|
|
91
|
+
$OUTPUT
|
|
92
|
+
</terminal_output>
|
|
93
|
+
|
|
94
|
+
Respond with EXACTLY one of these words (nothing else):
|
|
95
|
+
- FORGOT_COMPLETION — if the agent clearly finished its implementation work (closed beads, committed code, ran tests) but stopped without calling 'pan work done'
|
|
96
|
+
- STILL_WORKING — if the agent appears to have more work to do (mentioned next steps, was mid-task)
|
|
97
|
+
- STOPPED_FOR_INPUT — if the agent stopped because it needs human input or hit a blocker
|
|
98
|
+
- UNCLEAR — if you cannot determine the state"
|
|
99
|
+
|
|
100
|
+
# Run the analysis using claude CLI (headless, no interactive session)
|
|
101
|
+
RESULT=$(echo "$ANALYSIS_PROMPT" | claude -p --model "$COMPLETION_MODEL" --max-tokens 20 2>/dev/null || echo "UNCLEAR")
|
|
102
|
+
|
|
103
|
+
# Extract just the verdict (first word of output, strip whitespace)
|
|
104
|
+
VERDICT=$(echo "$RESULT" | tr -d '[:space:]' | head -c 30)
|
|
105
|
+
|
|
106
|
+
# Log the check
|
|
107
|
+
LOG_DIR="$HOME/.panopticon/logs"
|
|
108
|
+
mkdir -p "$LOG_DIR"
|
|
109
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: $AGENT_ID ($ISSUE_ID) -> $VERDICT (model: $COMPLETION_MODEL)" \
|
|
110
|
+
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
111
|
+
|
|
112
|
+
if [ "$VERDICT" = "FORGOT_COMPLETION" ]; then
|
|
113
|
+
# Record nudge timestamp for cooldown
|
|
114
|
+
mkdir -p "$(dirname "$NUDGE_FILE")"
|
|
115
|
+
date +%s > "$NUDGE_FILE" 2>/dev/null || true
|
|
116
|
+
|
|
117
|
+
# Write the nudge message to a temp file and use load-buffer + paste-buffer
|
|
118
|
+
# (the reliable tmux message delivery pattern from CLAUDE.md)
|
|
119
|
+
NUDGE_MSG="You stopped without calling pan work done. If your implementation is complete, you MUST run this command now:
|
|
120
|
+
|
|
121
|
+
pan work done $ISSUE_ID -c \"Implementation complete\"
|
|
122
|
+
|
|
123
|
+
If you still have remaining tasks, continue working on them. Do NOT stop until all work is done AND you have called pan work done."
|
|
124
|
+
|
|
125
|
+
TMPFILE=$(mktemp)
|
|
126
|
+
echo "$NUDGE_MSG" > "$TMPFILE"
|
|
127
|
+
tmux load-buffer "$TMPFILE" 2>/dev/null
|
|
128
|
+
tmux paste-buffer -t "$AGENT_ID" 2>/dev/null
|
|
129
|
+
sleep 0.3
|
|
130
|
+
tmux send-keys -t "$AGENT_ID" C-m 2>/dev/null
|
|
131
|
+
rm -f "$TMPFILE"
|
|
132
|
+
|
|
133
|
+
echo "[$(date -Iseconds)] work-agent-stop-hook: Sent completion nudge to $AGENT_ID" \
|
|
134
|
+
>> "$LOG_DIR/hooks.log" 2>/dev/null || true
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
exit 0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: check-merged
|
|
3
|
+
description: >
|
|
4
|
+
Verify whether an issue's feature branch has been merged into main.
|
|
5
|
+
Checks git history, branch existence, and commit presence. Returns
|
|
6
|
+
MERGED, NOT_MERGED, or BRANCH_NOT_FOUND with evidence.
|
|
7
|
+
Designed for cheap models (Haiku) to run quickly.
|
|
8
|
+
tools: Bash(git:*)
|
|
9
|
+
model: haiku
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Check Merged
|
|
13
|
+
|
|
14
|
+
Verify whether a feature branch for an issue has been merged into the main branch.
|
|
15
|
+
|
|
16
|
+
## Purpose
|
|
17
|
+
|
|
18
|
+
After an issue reaches "Done" on the kanban board, we need to confirm the code was actually merged — not just that the tracker status was updated. This skill checks git evidence to give a definitive answer.
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
|
|
22
|
+
- **Close-out ceremony**: Before archiving workspace artifacts
|
|
23
|
+
- **Done column audit**: Batch-verify all Done items are genuinely merged
|
|
24
|
+
- **Stale branch cleanup**: Identify branches that were abandoned vs merged
|
|
25
|
+
- **As a subagent**: Spawn from a parent agent to verify multiple issues in parallel
|
|
26
|
+
|
|
27
|
+
## Input
|
|
28
|
+
|
|
29
|
+
The skill expects an issue ID (e.g., `PAN-123`, `MIN-456`) and a project path.
|
|
30
|
+
|
|
31
|
+
## Execution
|
|
32
|
+
|
|
33
|
+
### Step 1: Resolve Branch Name
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Standard naming convention
|
|
37
|
+
BRANCH="feature/${ISSUE_ID_LOWER}"
|
|
38
|
+
|
|
39
|
+
# Also check alternate patterns
|
|
40
|
+
# edwardbecker/${issue-slug} (Linear default)
|
|
41
|
+
# feature/${issue-id}-description
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Step 2: Check If Branch Exists
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Check local branches
|
|
48
|
+
git -C "$PROJECT_PATH" branch --list "$BRANCH" 2>/dev/null
|
|
49
|
+
|
|
50
|
+
# Check remote branches
|
|
51
|
+
git -C "$PROJECT_PATH" ls-remote --heads origin "$BRANCH" 2>/dev/null
|
|
52
|
+
|
|
53
|
+
# Check Linear-style branch names (broader search)
|
|
54
|
+
git -C "$PROJECT_PATH" branch -a --list "*${ISSUE_ID_LOWER}*" 2>/dev/null
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Step 3: Check Merge Evidence
|
|
58
|
+
|
|
59
|
+
**If branch exists locally:**
|
|
60
|
+
```bash
|
|
61
|
+
# Check for unmerged commits
|
|
62
|
+
git -C "$PROJECT_PATH" log main.."$BRANCH" --oneline 2>/dev/null
|
|
63
|
+
# Empty output = fully merged
|
|
64
|
+
# Non-empty = has unmerged commits
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**If branch exists on remote only:**
|
|
68
|
+
```bash
|
|
69
|
+
git -C "$PROJECT_PATH" fetch origin "$BRANCH" 2>/dev/null
|
|
70
|
+
git -C "$PROJECT_PATH" log main..origin/"$BRANCH" --oneline 2>/dev/null
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**If no branch found (may be squash-merged and deleted):**
|
|
74
|
+
```bash
|
|
75
|
+
# Check if any commit in main references the issue ID
|
|
76
|
+
git -C "$PROJECT_PATH" log main --oneline --grep="$ISSUE_ID" 2>/dev/null
|
|
77
|
+
|
|
78
|
+
# Check merge commits
|
|
79
|
+
git -C "$PROJECT_PATH" log main --oneline --merges --grep="$ISSUE_ID" 2>/dev/null
|
|
80
|
+
|
|
81
|
+
# Check squash commits (PR title often contains issue ID)
|
|
82
|
+
git -C "$PROJECT_PATH" log main --oneline --grep="$BRANCH" 2>/dev/null
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Step 4: Check PR Status (if gh available)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Find closed/merged PRs for this branch
|
|
89
|
+
gh pr list --repo OWNER/REPO --state merged --head "$BRANCH" --json number,title,mergedAt 2>/dev/null
|
|
90
|
+
|
|
91
|
+
# Or search by issue reference
|
|
92
|
+
gh pr list --repo OWNER/REPO --state merged --search "$ISSUE_ID" --json number,title,mergedAt 2>/dev/null
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Output Format
|
|
96
|
+
|
|
97
|
+
Report one of three results:
|
|
98
|
+
|
|
99
|
+
### MERGED
|
|
100
|
+
```
|
|
101
|
+
RESULT: MERGED
|
|
102
|
+
ISSUE: PAN-123
|
|
103
|
+
EVIDENCE:
|
|
104
|
+
- Branch: feature/pan-123 (deleted after merge)
|
|
105
|
+
- Commit: abc1234 "PAN-123: Implement feature X" found on main
|
|
106
|
+
- PR: #456 merged at 2026-02-15T10:30:00Z
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### NOT_MERGED
|
|
110
|
+
```
|
|
111
|
+
RESULT: NOT_MERGED
|
|
112
|
+
ISSUE: PAN-123
|
|
113
|
+
EVIDENCE:
|
|
114
|
+
- Branch: feature/pan-123 exists with 3 unmerged commits
|
|
115
|
+
- Latest commit: def5678 "WIP: partial implementation"
|
|
116
|
+
- No merged PR found
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### BRANCH_NOT_FOUND
|
|
120
|
+
```
|
|
121
|
+
RESULT: BRANCH_NOT_FOUND
|
|
122
|
+
ISSUE: PAN-123
|
|
123
|
+
EVIDENCE:
|
|
124
|
+
- No branch matching "feature/pan-123" or "*pan-123*" found
|
|
125
|
+
- No commits on main reference "PAN-123"
|
|
126
|
+
- No merged PRs reference "PAN-123"
|
|
127
|
+
NOTE: Issue may have been completed without code changes, or branch name doesn't follow convention
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Polyrepo Support
|
|
131
|
+
|
|
132
|
+
For polyrepo projects, check each repo in the project:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Get repo list from project config
|
|
136
|
+
# For each repo, run the same checks against that repo's path
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Error Handling
|
|
140
|
+
|
|
141
|
+
- If `PROJECT_PATH` doesn't exist or isn't a git repo: report error immediately
|
|
142
|
+
- If `git fetch` fails (network): report based on local evidence only, note the fetch failure
|
|
143
|
+
- If `gh` CLI isn't available: skip PR check, report based on git evidence only
|