juno-code 1.0.44 → 1.0.46
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 +1 -1
- package/dist/bin/cli.js +658 -50
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +658 -50
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -4
- package/dist/index.mjs.map +1 -1
- package/dist/templates/scripts/__pycache__/attachment_downloader.cpython-38.pyc +0 -0
- package/dist/templates/scripts/__pycache__/github.cpython-38.pyc +0 -0
- package/dist/templates/scripts/__pycache__/slack_fetch.cpython-38.pyc +0 -0
- package/dist/templates/scripts/__pycache__/slack_state.cpython-38.pyc +0 -0
- package/dist/templates/scripts/attachment_downloader.py +405 -0
- package/dist/templates/scripts/github.py +282 -7
- package/dist/templates/scripts/hooks/session_counter.sh +328 -0
- package/dist/templates/scripts/kanban.sh +22 -4
- package/dist/templates/scripts/log_scanner.sh +790 -0
- package/dist/templates/scripts/slack_fetch.py +232 -20
- package/dist/templates/services/claude.py +50 -1
- package/dist/templates/services/codex.py +5 -4
- package/dist/templates/skills/claude/.gitkeep +0 -0
- package/dist/templates/skills/claude/plan-kanban-tasks/SKILL.md +25 -0
- package/dist/templates/skills/claude/ralph-loop/SKILL.md +43 -0
- package/dist/templates/skills/claude/ralph-loop/references/first_check.md +20 -0
- package/dist/templates/skills/claude/ralph-loop/references/implement.md +99 -0
- package/dist/templates/skills/claude/ralph-loop/scripts/kanban.sh +293 -0
- package/dist/templates/skills/claude/understand-project/SKILL.md +39 -0
- package/dist/templates/skills/codex/.gitkeep +0 -0
- package/dist/templates/skills/codex/ralph-loop/SKILL.md +43 -0
- package/dist/templates/skills/codex/ralph-loop/references/first_check.md +20 -0
- package/dist/templates/skills/codex/ralph-loop/references/implement.md +99 -0
- package/dist/templates/skills/codex/ralph-loop/scripts/kanban.sh +293 -0
- package/package.json +3 -2
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# log_scanner.sh
|
|
4
|
+
#
|
|
5
|
+
# Purpose: Scan log files for errors/exceptions and create kanban bug reports
|
|
6
|
+
#
|
|
7
|
+
# Uses ripgrep for high-performance scanning. Detects common error patterns
|
|
8
|
+
# across Python (FastAPI, Django, SQLAlchemy, Postgres), Node.js (Express,
|
|
9
|
+
# TypeScript), and general application log formats.
|
|
10
|
+
#
|
|
11
|
+
# Usage: ./.juno_task/scripts/log_scanner.sh [OPTIONS]
|
|
12
|
+
#
|
|
13
|
+
# Options:
|
|
14
|
+
# --scan-dir <dir> Directory to scan (default: project root)
|
|
15
|
+
# --state-file <path> Path to timestamp state file (default: .juno_task/.log_scanner_state)
|
|
16
|
+
# --dry-run Show what would be reported without creating tasks
|
|
17
|
+
# --reset Reset the last-checked timestamp (scan everything)
|
|
18
|
+
# --status Show current scanner state
|
|
19
|
+
# --verbose Show detailed progress output
|
|
20
|
+
# --help, -h Show this help message
|
|
21
|
+
#
|
|
22
|
+
# Environment Variables:
|
|
23
|
+
# LOG_SCANNER_LAST_CHECKED Override the last-checked timestamp (ISO 8601)
|
|
24
|
+
# LOG_SCANNER_STATE_FILE Override state file path
|
|
25
|
+
# LOG_SCANNER_SCAN_DIR Override scan directory
|
|
26
|
+
# LOG_SCANNER_MAX_TASKS Max kanban tasks to create per run (default: 10)
|
|
27
|
+
# LOG_SCANNER_LOG_GLOBS Comma-separated glob patterns for log files
|
|
28
|
+
# (default: *.log,*.log.*,*.err)
|
|
29
|
+
# JUNO_DEBUG Enable debug output when set to "true"
|
|
30
|
+
#
|
|
31
|
+
# Created by: juno-code init command
|
|
32
|
+
# Date: Auto-generated during project initialization
|
|
33
|
+
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
VERSION="1.0.0"
|
|
37
|
+
|
|
38
|
+
# ── Colours ──────────────────────────────────────────────────────────────────
|
|
39
|
+
RED='\033[0;31m'
|
|
40
|
+
GREEN='\033[0;32m'
|
|
41
|
+
YELLOW='\033[1;33m'
|
|
42
|
+
BLUE='\033[0;34m'
|
|
43
|
+
CYAN='\033[0;36m'
|
|
44
|
+
NC='\033[0m'
|
|
45
|
+
|
|
46
|
+
# ── Defaults ─────────────────────────────────────────────────────────────────
|
|
47
|
+
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
48
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../.." && pwd )"
|
|
49
|
+
KANBAN_SCRIPT="$SCRIPT_DIR/kanban.sh"
|
|
50
|
+
|
|
51
|
+
DEFAULT_STATE_FILE=".juno_task/.log_scanner_state"
|
|
52
|
+
DEFAULT_SCAN_DIR="$PROJECT_ROOT"
|
|
53
|
+
DEFAULT_MAX_TASKS=10
|
|
54
|
+
DEFAULT_LOG_GLOBS="*.log,*.log.*,*.err"
|
|
55
|
+
|
|
56
|
+
STATE_FILE="${LOG_SCANNER_STATE_FILE:-$DEFAULT_STATE_FILE}"
|
|
57
|
+
SCAN_DIR="${LOG_SCANNER_SCAN_DIR:-$DEFAULT_SCAN_DIR}"
|
|
58
|
+
MAX_TASKS="${LOG_SCANNER_MAX_TASKS:-$DEFAULT_MAX_TASKS}"
|
|
59
|
+
LOG_GLOBS="${LOG_SCANNER_LOG_GLOBS:-$DEFAULT_LOG_GLOBS}"
|
|
60
|
+
|
|
61
|
+
# Modes
|
|
62
|
+
DRY_RUN=false
|
|
63
|
+
RESET_MODE=false
|
|
64
|
+
STATUS_MODE=false
|
|
65
|
+
VERBOSE=false
|
|
66
|
+
|
|
67
|
+
# ── Logging ──────────────────────────────────────────────────────────────────
|
|
68
|
+
log_debug() {
|
|
69
|
+
if [ "${JUNO_DEBUG:-false}" = "true" ]; then
|
|
70
|
+
echo -e "${CYAN}[DEBUG]${NC} $1" >&2
|
|
71
|
+
fi
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log_info() {
|
|
75
|
+
if [ "$VERBOSE" = "true" ]; then
|
|
76
|
+
echo -e "${BLUE}[LOG_SCANNER]${NC} $1" >&2
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log_warn() {
|
|
81
|
+
echo -e "${YELLOW}[LOG_SCANNER]${NC} $1" >&2
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log_error() {
|
|
85
|
+
echo -e "${RED}[LOG_SCANNER]${NC} $1" >&2
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log_success() {
|
|
89
|
+
echo -e "${GREEN}[LOG_SCANNER]${NC} $1" >&2
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# ── Help ─────────────────────────────────────────────────────────────────────
|
|
93
|
+
show_help() {
|
|
94
|
+
cat << 'HELPEOF'
|
|
95
|
+
log_scanner.sh - Scan log files for errors and create kanban bug reports
|
|
96
|
+
|
|
97
|
+
SYNOPSIS
|
|
98
|
+
log_scanner.sh [OPTIONS]
|
|
99
|
+
|
|
100
|
+
DESCRIPTION
|
|
101
|
+
Scans log files in the project for errors, exceptions, tracebacks, and
|
|
102
|
+
other failure indicators. When issues are found, creates kanban tasks
|
|
103
|
+
with context (surrounding lines) so an agent can investigate.
|
|
104
|
+
|
|
105
|
+
Uses ripgrep (rg) for high-performance searching. Falls back to grep
|
|
106
|
+
if ripgrep is not installed.
|
|
107
|
+
|
|
108
|
+
Only scans entries newer than the last scan to avoid duplicate reports.
|
|
109
|
+
|
|
110
|
+
OPTIONS
|
|
111
|
+
--scan-dir <dir>
|
|
112
|
+
Directory to scan for log files. Default: project root
|
|
113
|
+
|
|
114
|
+
--state-file <path>
|
|
115
|
+
Path to the state file that tracks last scan time.
|
|
116
|
+
Default: .juno_task/.log_scanner_state
|
|
117
|
+
|
|
118
|
+
--dry-run
|
|
119
|
+
Show what errors would be reported without creating kanban tasks.
|
|
120
|
+
|
|
121
|
+
--reset
|
|
122
|
+
Clear the last-checked timestamp so the next run scans all entries.
|
|
123
|
+
|
|
124
|
+
--status
|
|
125
|
+
Show current scanner state (last scan time, log file count).
|
|
126
|
+
|
|
127
|
+
--verbose
|
|
128
|
+
Show detailed progress during scanning.
|
|
129
|
+
|
|
130
|
+
--help, -h
|
|
131
|
+
Show this help message.
|
|
132
|
+
|
|
133
|
+
ENVIRONMENT VARIABLES
|
|
134
|
+
LOG_SCANNER_LAST_CHECKED
|
|
135
|
+
Override the last-checked timestamp (ISO 8601 format).
|
|
136
|
+
Takes precedence over the state file.
|
|
137
|
+
|
|
138
|
+
LOG_SCANNER_STATE_FILE
|
|
139
|
+
Override the state file path.
|
|
140
|
+
|
|
141
|
+
LOG_SCANNER_SCAN_DIR
|
|
142
|
+
Override the directory to scan.
|
|
143
|
+
|
|
144
|
+
LOG_SCANNER_MAX_TASKS
|
|
145
|
+
Maximum kanban tasks to create per run. Default: 10
|
|
146
|
+
|
|
147
|
+
LOG_SCANNER_LOG_GLOBS
|
|
148
|
+
Comma-separated glob patterns for log files.
|
|
149
|
+
Default: *.log,*.log.*,*.err
|
|
150
|
+
|
|
151
|
+
JUNO_DEBUG
|
|
152
|
+
Set to "true" for debug output.
|
|
153
|
+
|
|
154
|
+
DETECTED PATTERNS
|
|
155
|
+
Python:
|
|
156
|
+
- Traceback (most recent call last)
|
|
157
|
+
- Exception, Error suffixed classes (ValueError, TypeError, etc.)
|
|
158
|
+
- FastAPI/Starlette errors, HTTPException
|
|
159
|
+
- SQLAlchemy/database errors (IntegrityError, OperationalError)
|
|
160
|
+
- PostgreSQL errors (FATAL, ERROR, PANIC)
|
|
161
|
+
- Django errors (ImproperlyConfigured, etc.)
|
|
162
|
+
|
|
163
|
+
Node.js:
|
|
164
|
+
- Unhandled promise rejection, uncaught exception
|
|
165
|
+
- TypeError, ReferenceError, SyntaxError, RangeError
|
|
166
|
+
- ECONNREFUSED, ENOENT, EACCES, ETIMEDOUT
|
|
167
|
+
- Express/Fastify errors
|
|
168
|
+
- Stack traces (at Module., at Object., at Function.)
|
|
169
|
+
|
|
170
|
+
General:
|
|
171
|
+
- FATAL, CRITICAL, PANIC log levels
|
|
172
|
+
- ERROR log level (with common log formats)
|
|
173
|
+
- Segmentation fault, core dump, out of memory
|
|
174
|
+
- Connection refused/timeout/reset
|
|
175
|
+
- Permission denied, access denied
|
|
176
|
+
- Killed, OOMKilled
|
|
177
|
+
|
|
178
|
+
EXAMPLES
|
|
179
|
+
# Scan project logs (first run scans everything)
|
|
180
|
+
./.juno_task/scripts/log_scanner.sh
|
|
181
|
+
|
|
182
|
+
# Dry run to preview what would be reported
|
|
183
|
+
./.juno_task/scripts/log_scanner.sh --dry-run --verbose
|
|
184
|
+
|
|
185
|
+
# Scan a specific directory
|
|
186
|
+
./.juno_task/scripts/log_scanner.sh --scan-dir /var/log/myapp
|
|
187
|
+
|
|
188
|
+
# Reset and re-scan all entries
|
|
189
|
+
./.juno_task/scripts/log_scanner.sh --reset && ./.juno_task/scripts/log_scanner.sh
|
|
190
|
+
|
|
191
|
+
# Use as a pre-run hook in config.json
|
|
192
|
+
# {
|
|
193
|
+
# "hooks": {
|
|
194
|
+
# "START_ITERATION": {
|
|
195
|
+
# "commands": ["./.juno_task/scripts/log_scanner.sh --verbose"]
|
|
196
|
+
# }
|
|
197
|
+
# }
|
|
198
|
+
# }
|
|
199
|
+
|
|
200
|
+
VERSION
|
|
201
|
+
1.0.0
|
|
202
|
+
|
|
203
|
+
SEE ALSO
|
|
204
|
+
kanban.sh, run_until_completion.sh
|
|
205
|
+
HELPEOF
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# ── Argument parsing ─────────────────────────────────────────────────────────
|
|
209
|
+
while [[ $# -gt 0 ]]; do
|
|
210
|
+
case $1 in
|
|
211
|
+
--scan-dir)
|
|
212
|
+
if [[ -n "${2:-}" ]]; then
|
|
213
|
+
SCAN_DIR="$2"
|
|
214
|
+
shift 2
|
|
215
|
+
else
|
|
216
|
+
log_error "--scan-dir requires a directory path"
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
;;
|
|
220
|
+
--state-file)
|
|
221
|
+
if [[ -n "${2:-}" ]]; then
|
|
222
|
+
STATE_FILE="$2"
|
|
223
|
+
shift 2
|
|
224
|
+
else
|
|
225
|
+
log_error "--state-file requires a file path"
|
|
226
|
+
exit 1
|
|
227
|
+
fi
|
|
228
|
+
;;
|
|
229
|
+
--dry-run)
|
|
230
|
+
DRY_RUN=true
|
|
231
|
+
shift
|
|
232
|
+
;;
|
|
233
|
+
--reset)
|
|
234
|
+
RESET_MODE=true
|
|
235
|
+
shift
|
|
236
|
+
;;
|
|
237
|
+
--status)
|
|
238
|
+
STATUS_MODE=true
|
|
239
|
+
shift
|
|
240
|
+
;;
|
|
241
|
+
--verbose|-v)
|
|
242
|
+
VERBOSE=true
|
|
243
|
+
shift
|
|
244
|
+
;;
|
|
245
|
+
--help|-h)
|
|
246
|
+
show_help
|
|
247
|
+
exit 0
|
|
248
|
+
;;
|
|
249
|
+
--version)
|
|
250
|
+
echo "log_scanner.sh version $VERSION"
|
|
251
|
+
exit 0
|
|
252
|
+
;;
|
|
253
|
+
*)
|
|
254
|
+
log_warn "Unknown option: $1 (ignored)"
|
|
255
|
+
shift
|
|
256
|
+
;;
|
|
257
|
+
esac
|
|
258
|
+
done
|
|
259
|
+
|
|
260
|
+
# ── Resolve paths ────────────────────────────────────────────────────────────
|
|
261
|
+
cd "$PROJECT_ROOT"
|
|
262
|
+
|
|
263
|
+
# Make STATE_FILE absolute if relative
|
|
264
|
+
if [[ "$STATE_FILE" != /* ]]; then
|
|
265
|
+
STATE_FILE="$PROJECT_ROOT/$STATE_FILE"
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
# Make SCAN_DIR absolute if relative
|
|
269
|
+
if [[ "$SCAN_DIR" != /* ]]; then
|
|
270
|
+
SCAN_DIR="$PROJECT_ROOT/$SCAN_DIR"
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
# ── Prereqs ──────────────────────────────────────────────────────────────────
|
|
274
|
+
# Detect search tool: prefer ripgrep for speed
|
|
275
|
+
SEARCH_CMD=""
|
|
276
|
+
if command -v rg &>/dev/null; then
|
|
277
|
+
SEARCH_CMD="rg"
|
|
278
|
+
log_debug "Using ripgrep (rg) for search"
|
|
279
|
+
elif command -v grep &>/dev/null; then
|
|
280
|
+
SEARCH_CMD="grep"
|
|
281
|
+
log_warn "ripgrep not found, falling back to grep (slower)"
|
|
282
|
+
else
|
|
283
|
+
log_error "Neither ripgrep (rg) nor grep found. Cannot scan logs."
|
|
284
|
+
exit 1
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# ── State management ────────────────────────────────────────────────────────
|
|
288
|
+
get_last_checked() {
|
|
289
|
+
# Priority: ENV > state file > epoch
|
|
290
|
+
if [[ -n "${LOG_SCANNER_LAST_CHECKED:-}" ]]; then
|
|
291
|
+
echo "$LOG_SCANNER_LAST_CHECKED"
|
|
292
|
+
return
|
|
293
|
+
fi
|
|
294
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
295
|
+
cat "$STATE_FILE" 2>/dev/null || echo ""
|
|
296
|
+
else
|
|
297
|
+
echo ""
|
|
298
|
+
fi
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
save_last_checked() {
|
|
302
|
+
local timestamp="$1"
|
|
303
|
+
local state_dir
|
|
304
|
+
state_dir="$(dirname "$STATE_FILE")"
|
|
305
|
+
mkdir -p "$state_dir" 2>/dev/null || true
|
|
306
|
+
echo "$timestamp" > "$STATE_FILE"
|
|
307
|
+
log_debug "Saved last-checked timestamp: $timestamp"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Convert ISO timestamp to epoch seconds (cross-platform)
|
|
311
|
+
timestamp_to_epoch() {
|
|
312
|
+
local ts="$1"
|
|
313
|
+
if [[ -z "$ts" ]]; then
|
|
314
|
+
echo "0"
|
|
315
|
+
return
|
|
316
|
+
fi
|
|
317
|
+
# Try GNU date first, then BSD date
|
|
318
|
+
if date -d "$ts" +%s 2>/dev/null; then
|
|
319
|
+
return
|
|
320
|
+
elif date -j -f "%Y-%m-%dT%H:%M:%S" "$ts" +%s 2>/dev/null; then
|
|
321
|
+
return
|
|
322
|
+
elif date -j -f "%Y-%m-%d %H:%M:%S" "$ts" +%s 2>/dev/null; then
|
|
323
|
+
return
|
|
324
|
+
else
|
|
325
|
+
echo "0"
|
|
326
|
+
fi
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Get current time as ISO 8601
|
|
330
|
+
now_iso() {
|
|
331
|
+
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# Get file modification time as epoch (cross-platform)
|
|
335
|
+
file_mtime_epoch() {
|
|
336
|
+
local filepath="$1"
|
|
337
|
+
if stat -c %Y "$filepath" 2>/dev/null; then
|
|
338
|
+
return
|
|
339
|
+
elif stat -f %m "$filepath" 2>/dev/null; then
|
|
340
|
+
return
|
|
341
|
+
else
|
|
342
|
+
echo "0"
|
|
343
|
+
fi
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# ── Handle modes ─────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
# Handle --reset
|
|
349
|
+
if [[ "$RESET_MODE" = "true" ]]; then
|
|
350
|
+
if [[ -f "$STATE_FILE" ]]; then
|
|
351
|
+
rm "$STATE_FILE"
|
|
352
|
+
log_success "Last-checked timestamp reset. Next run will scan all log entries."
|
|
353
|
+
else
|
|
354
|
+
log_info "No state file found. Nothing to reset."
|
|
355
|
+
fi
|
|
356
|
+
exit 0
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# Handle --status
|
|
360
|
+
if [[ "$STATUS_MODE" = "true" ]]; then
|
|
361
|
+
echo "=== Log Scanner Status ===" >&2
|
|
362
|
+
echo "Scan directory: $SCAN_DIR" >&2
|
|
363
|
+
echo "State file: $STATE_FILE" >&2
|
|
364
|
+
echo "Max tasks per run: $MAX_TASKS" >&2
|
|
365
|
+
echo "Log globs: $LOG_GLOBS" >&2
|
|
366
|
+
echo "Search tool: $SEARCH_CMD" >&2
|
|
367
|
+
|
|
368
|
+
last_ts=$(get_last_checked)
|
|
369
|
+
if [[ -n "$last_ts" ]]; then
|
|
370
|
+
echo "Last scan: $last_ts" >&2
|
|
371
|
+
else
|
|
372
|
+
echo "Last scan: never (will scan all entries)" >&2
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
# Count log files
|
|
376
|
+
log_count=0
|
|
377
|
+
IFS=',' read -ra GLOBS <<< "$LOG_GLOBS"
|
|
378
|
+
for glob in "${GLOBS[@]}"; do
|
|
379
|
+
glob=$(echo "$glob" | xargs) # trim whitespace
|
|
380
|
+
count=$(find "$SCAN_DIR" -name "$glob" -type f 2>/dev/null | wc -l | xargs)
|
|
381
|
+
log_count=$((log_count + count))
|
|
382
|
+
done
|
|
383
|
+
echo "Log files found: $log_count" >&2
|
|
384
|
+
exit 0
|
|
385
|
+
fi
|
|
386
|
+
|
|
387
|
+
# ── Build the error pattern ─────────────────────────────────────────────────
|
|
388
|
+
#
|
|
389
|
+
# Single comprehensive regex pattern for ripgrep.
|
|
390
|
+
# We use alternation (|) to combine all patterns into one rg call for speed.
|
|
391
|
+
# The pattern is case-insensitive for some parts but case-sensitive for others,
|
|
392
|
+
# so we use two passes: one case-insensitive for general terms, one
|
|
393
|
+
# case-sensitive for specific class names.
|
|
394
|
+
|
|
395
|
+
# Case-INSENSITIVE patterns (general error indicators)
|
|
396
|
+
# These catch error/exception mentions regardless of casing
|
|
397
|
+
PATTERN_INSENSITIVE=$(cat << 'PATEOF'
|
|
398
|
+
(fatal|panic|critical)\s*[:\]|]
|
|
399
|
+
|segmentation\s+fault
|
|
400
|
+
|core\s+dump(ed)?
|
|
401
|
+
|out\s+of\s+memory
|
|
402
|
+
|oom\s*kill
|
|
403
|
+
|killed\s+process
|
|
404
|
+
|connection\s+(refused|timed?\s*out|reset)
|
|
405
|
+
|permission\s+denied
|
|
406
|
+
|access\s+denied
|
|
407
|
+
|stack\s*overflow
|
|
408
|
+
|deadlock\s+detected
|
|
409
|
+
|disk\s+full
|
|
410
|
+
|no\s+space\s+left
|
|
411
|
+
|too\s+many\s+open\s+files
|
|
412
|
+
PATEOF
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Case-SENSITIVE patterns (specific error classes and log formats)
|
|
416
|
+
PATTERN_SENSITIVE=$(cat << 'PATEOF'
|
|
417
|
+
Traceback \(most recent call last\)
|
|
418
|
+
|^\s*(raise\s+)?\w*Error(\(|:|\s)
|
|
419
|
+
|^\s*(raise\s+)?\w*Exception(\(|:|\s)
|
|
420
|
+
|\b(ValueError|TypeError|KeyError|AttributeError|ImportError|ModuleNotFoundError)\b
|
|
421
|
+
|\b(RuntimeError|StopIteration|IndexError|NameError|FileNotFoundError)\b
|
|
422
|
+
|\b(ConnectionError|TimeoutError|OSError|IOError|PermissionError)\b
|
|
423
|
+
|\b(IntegrityError|OperationalError|ProgrammingError|DataError|DatabaseError)\b
|
|
424
|
+
|\b(HTTPException|RequestValidationError|ValidationError)\b
|
|
425
|
+
|\b(ImproperlyConfigured|ObjectDoesNotExist)\b
|
|
426
|
+
|\bERROR\b\s*[\[|\]:]
|
|
427
|
+
|\bFATAL\b\s*[\[|\]:]
|
|
428
|
+
|\bPANIC\b\s*[\[|\]:]
|
|
429
|
+
|\bCRITICAL\b\s*[\[|\]:]
|
|
430
|
+
|\bUnhandledPromiseRejection\b
|
|
431
|
+
|\buncaughtException\b
|
|
432
|
+
|\bUnhandled\s+rejection\b
|
|
433
|
+
|\b(ECONNREFUSED|ENOENT|EACCES|ETIMEDOUT|EADDRINUSE|EPERM|ENOMEM)\b
|
|
434
|
+
|\bReferenceError\b
|
|
435
|
+
|\bSyntaxError\b
|
|
436
|
+
|\bRangeError\b
|
|
437
|
+
|\bURIError\b
|
|
438
|
+
|\bEvalError\b
|
|
439
|
+
|\bAggregateError\b
|
|
440
|
+
|ERR!\s
|
|
441
|
+
|npm\s+ERR!
|
|
442
|
+
|Error:\s+Cannot\s+find\s+module
|
|
443
|
+
|SIGKILL|SIGTERM|SIGSEGV|SIGABRT
|
|
444
|
+
PATEOF
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Clean up patterns: remove newlines to form single-line regex
|
|
448
|
+
PATTERN_INSENSITIVE=$(echo "$PATTERN_INSENSITIVE" | tr '\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//')
|
|
449
|
+
PATTERN_SENSITIVE=$(echo "$PATTERN_SENSITIVE" | tr '\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//')
|
|
450
|
+
|
|
451
|
+
# ── Build glob arguments ────────────────────────────────────────────────────
|
|
452
|
+
build_glob_args() {
|
|
453
|
+
local globs_csv="$1"
|
|
454
|
+
local args=()
|
|
455
|
+
IFS=',' read -ra GLOBS <<< "$globs_csv"
|
|
456
|
+
for glob in "${GLOBS[@]}"; do
|
|
457
|
+
glob=$(echo "$glob" | xargs) # trim
|
|
458
|
+
if [[ "$SEARCH_CMD" == "rg" ]]; then
|
|
459
|
+
args+=("--glob" "$glob")
|
|
460
|
+
fi
|
|
461
|
+
done
|
|
462
|
+
echo "${args[@]}"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# ── Scan for errors ─────────────────────────────────────────────────────────
|
|
466
|
+
# Collects matching lines with context into a temporary results file.
|
|
467
|
+
# Each "hit" is a block of lines (match + context).
|
|
468
|
+
|
|
469
|
+
scan_logs() {
|
|
470
|
+
local results_file="$1"
|
|
471
|
+
local last_checked_ts="$2"
|
|
472
|
+
local last_checked_epoch=0
|
|
473
|
+
local total_hits=0
|
|
474
|
+
|
|
475
|
+
if [[ -n "$last_checked_ts" ]]; then
|
|
476
|
+
last_checked_epoch=$(timestamp_to_epoch "$last_checked_ts")
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
log_info "Scanning directory: $SCAN_DIR"
|
|
480
|
+
log_info "Last checked: ${last_checked_ts:-never}"
|
|
481
|
+
|
|
482
|
+
# Build glob arguments for rg
|
|
483
|
+
local glob_args
|
|
484
|
+
IFS=' ' read -ra glob_args <<< "$(build_glob_args "$LOG_GLOBS")"
|
|
485
|
+
|
|
486
|
+
# Collect log files and filter by modification time
|
|
487
|
+
local log_files=()
|
|
488
|
+
IFS=',' read -ra GLOBS <<< "$LOG_GLOBS"
|
|
489
|
+
for glob in "${GLOBS[@]}"; do
|
|
490
|
+
glob=$(echo "$glob" | xargs)
|
|
491
|
+
while IFS= read -r f; do
|
|
492
|
+
if [[ -n "$f" && -f "$f" ]]; then
|
|
493
|
+
# Filter by file modification time
|
|
494
|
+
local fmtime
|
|
495
|
+
fmtime=$(file_mtime_epoch "$f")
|
|
496
|
+
if [[ "$fmtime" -gt "$last_checked_epoch" ]] || [[ "$last_checked_epoch" -eq 0 ]]; then
|
|
497
|
+
log_files+=("$f")
|
|
498
|
+
fi
|
|
499
|
+
fi
|
|
500
|
+
done < <(find "$SCAN_DIR" -name "$glob" -type f \
|
|
501
|
+
-not -path "*/.git/*" \
|
|
502
|
+
-not -path "*/node_modules/*" \
|
|
503
|
+
-not -path "*/.venv*" \
|
|
504
|
+
-not -path "*/__pycache__/*" \
|
|
505
|
+
-not -path "*/dist/*" \
|
|
506
|
+
-not -path "*/.juno_task/*" \
|
|
507
|
+
2>/dev/null || true)
|
|
508
|
+
done
|
|
509
|
+
|
|
510
|
+
local file_count=${#log_files[@]}
|
|
511
|
+
log_info "Found $file_count log file(s) modified since last scan"
|
|
512
|
+
|
|
513
|
+
if [[ "$file_count" -eq 0 ]]; then
|
|
514
|
+
log_info "No log files to scan."
|
|
515
|
+
return 0
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# Run ripgrep (or grep) on each file
|
|
519
|
+
for logfile in "${log_files[@]}"; do
|
|
520
|
+
log_debug "Scanning: $logfile"
|
|
521
|
+
|
|
522
|
+
local rel_path
|
|
523
|
+
rel_path=$(realpath --relative-to="$PROJECT_ROOT" "$logfile" 2>/dev/null || echo "$logfile")
|
|
524
|
+
|
|
525
|
+
if [[ "$SEARCH_CMD" == "rg" ]]; then
|
|
526
|
+
# Case-insensitive pass
|
|
527
|
+
rg --no-heading --line-number --context 3 \
|
|
528
|
+
--ignore-case \
|
|
529
|
+
"$PATTERN_INSENSITIVE" \
|
|
530
|
+
"$logfile" 2>/dev/null | while IFS= read -r line; do
|
|
531
|
+
echo "FILE:$rel_path|$line"
|
|
532
|
+
done >> "$results_file" || true
|
|
533
|
+
|
|
534
|
+
# Case-sensitive pass
|
|
535
|
+
rg --no-heading --line-number --context 3 \
|
|
536
|
+
"$PATTERN_SENSITIVE" \
|
|
537
|
+
"$logfile" 2>/dev/null | while IFS= read -r line; do
|
|
538
|
+
echo "FILE:$rel_path|$line"
|
|
539
|
+
done >> "$results_file" || true
|
|
540
|
+
else
|
|
541
|
+
# grep fallback — case-insensitive pass
|
|
542
|
+
grep -n -i -E "$PATTERN_INSENSITIVE" \
|
|
543
|
+
-B 3 -A 3 \
|
|
544
|
+
"$logfile" 2>/dev/null | while IFS= read -r line; do
|
|
545
|
+
echo "FILE:$rel_path|$line"
|
|
546
|
+
done >> "$results_file" || true
|
|
547
|
+
|
|
548
|
+
# grep fallback — case-sensitive pass
|
|
549
|
+
grep -n -E "$PATTERN_SENSITIVE" \
|
|
550
|
+
-B 3 -A 3 \
|
|
551
|
+
"$logfile" 2>/dev/null | while IFS= read -r line; do
|
|
552
|
+
echo "FILE:$rel_path|$line"
|
|
553
|
+
done >> "$results_file" || true
|
|
554
|
+
fi
|
|
555
|
+
done
|
|
556
|
+
|
|
557
|
+
# Count unique matching blocks (separated by -- in rg/grep context output)
|
|
558
|
+
if [[ -f "$results_file" ]]; then
|
|
559
|
+
total_hits=$(grep -c "^FILE:" "$results_file" 2>/dev/null || echo "0")
|
|
560
|
+
fi
|
|
561
|
+
|
|
562
|
+
log_info "Found $total_hits matching line(s) across $file_count file(s)"
|
|
563
|
+
return 0
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
# ── Deduplicate and group results ────────────────────────────────────────────
|
|
567
|
+
# Groups results by file and creates distinct error blocks.
|
|
568
|
+
# Returns an array of error descriptions suitable for kanban tasks.
|
|
569
|
+
|
|
570
|
+
deduplicate_results() {
|
|
571
|
+
local results_file="$1"
|
|
572
|
+
local output_file="$2"
|
|
573
|
+
|
|
574
|
+
if [[ ! -f "$results_file" ]] || [[ ! -s "$results_file" ]]; then
|
|
575
|
+
return 0
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
# Group consecutive lines from the same file into blocks.
|
|
579
|
+
# rg/grep context separators (--) denote block boundaries.
|
|
580
|
+
local current_file=""
|
|
581
|
+
local current_block=""
|
|
582
|
+
local block_count=0
|
|
583
|
+
|
|
584
|
+
while IFS= read -r raw_line; do
|
|
585
|
+
if [[ "$raw_line" == *"--"* ]] && [[ ! "$raw_line" == FILE:* ]]; then
|
|
586
|
+
# Block separator — flush current block
|
|
587
|
+
if [[ -n "$current_block" && "$block_count" -lt "$MAX_TASKS" ]]; then
|
|
588
|
+
echo "---BLOCK---" >> "$output_file"
|
|
589
|
+
echo "file: $current_file" >> "$output_file"
|
|
590
|
+
echo "$current_block" >> "$output_file"
|
|
591
|
+
block_count=$((block_count + 1))
|
|
592
|
+
current_block=""
|
|
593
|
+
fi
|
|
594
|
+
continue
|
|
595
|
+
fi
|
|
596
|
+
|
|
597
|
+
if [[ "$raw_line" == FILE:* ]]; then
|
|
598
|
+
# Extract file path and content
|
|
599
|
+
local file_part="${raw_line#FILE:}"
|
|
600
|
+
local file_name="${file_part%%|*}"
|
|
601
|
+
local content="${file_part#*|}"
|
|
602
|
+
|
|
603
|
+
if [[ "$file_name" != "$current_file" && -n "$current_block" && "$block_count" -lt "$MAX_TASKS" ]]; then
|
|
604
|
+
# New file — flush previous block
|
|
605
|
+
echo "---BLOCK---" >> "$output_file"
|
|
606
|
+
echo "file: $current_file" >> "$output_file"
|
|
607
|
+
echo "$current_block" >> "$output_file"
|
|
608
|
+
block_count=$((block_count + 1))
|
|
609
|
+
current_block=""
|
|
610
|
+
fi
|
|
611
|
+
|
|
612
|
+
current_file="$file_name"
|
|
613
|
+
if [[ -n "$current_block" ]]; then
|
|
614
|
+
current_block="$current_block"$'\n'"$content"
|
|
615
|
+
else
|
|
616
|
+
current_block="$content"
|
|
617
|
+
fi
|
|
618
|
+
fi
|
|
619
|
+
done < "$results_file"
|
|
620
|
+
|
|
621
|
+
# Flush last block
|
|
622
|
+
if [[ -n "$current_block" && "$block_count" -lt "$MAX_TASKS" ]]; then
|
|
623
|
+
echo "---BLOCK---" >> "$output_file"
|
|
624
|
+
echo "file: $current_file" >> "$output_file"
|
|
625
|
+
echo "$current_block" >> "$output_file"
|
|
626
|
+
block_count=$((block_count + 1))
|
|
627
|
+
fi
|
|
628
|
+
|
|
629
|
+
log_info "Grouped into $block_count error block(s)"
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
# ── Create kanban tasks ──────────────────────────────────────────────────────
|
|
633
|
+
create_kanban_tasks() {
|
|
634
|
+
local blocks_file="$1"
|
|
635
|
+
local tasks_created=0
|
|
636
|
+
|
|
637
|
+
if [[ ! -f "$blocks_file" ]] || [[ ! -s "$blocks_file" ]]; then
|
|
638
|
+
log_info "No error blocks to report."
|
|
639
|
+
return 0
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
if [[ ! -f "$KANBAN_SCRIPT" ]]; then
|
|
643
|
+
log_error "kanban.sh not found at: $KANBAN_SCRIPT"
|
|
644
|
+
log_error "Cannot create tasks. Run 'juno-code init' first."
|
|
645
|
+
return 1
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
local current_file=""
|
|
649
|
+
local current_body=""
|
|
650
|
+
local in_block=false
|
|
651
|
+
|
|
652
|
+
while IFS= read -r line; do
|
|
653
|
+
if [[ "$line" == "---BLOCK---" ]]; then
|
|
654
|
+
# Flush previous block as a kanban task
|
|
655
|
+
if [[ "$in_block" = "true" && -n "$current_body" ]]; then
|
|
656
|
+
create_single_task "$current_file" "$current_body"
|
|
657
|
+
tasks_created=$((tasks_created + 1))
|
|
658
|
+
if [[ "$tasks_created" -ge "$MAX_TASKS" ]]; then
|
|
659
|
+
log_warn "Reached max tasks limit ($MAX_TASKS). Stopping."
|
|
660
|
+
break
|
|
661
|
+
fi
|
|
662
|
+
fi
|
|
663
|
+
current_file=""
|
|
664
|
+
current_body=""
|
|
665
|
+
in_block=true
|
|
666
|
+
continue
|
|
667
|
+
fi
|
|
668
|
+
|
|
669
|
+
if [[ "$line" == file:\ * ]]; then
|
|
670
|
+
current_file="${line#file: }"
|
|
671
|
+
continue
|
|
672
|
+
fi
|
|
673
|
+
|
|
674
|
+
if [[ "$in_block" = "true" ]]; then
|
|
675
|
+
if [[ -n "$current_body" ]]; then
|
|
676
|
+
current_body="$current_body"$'\n'"$line"
|
|
677
|
+
else
|
|
678
|
+
current_body="$line"
|
|
679
|
+
fi
|
|
680
|
+
fi
|
|
681
|
+
done < "$blocks_file"
|
|
682
|
+
|
|
683
|
+
# Flush last block
|
|
684
|
+
if [[ "$in_block" = "true" && -n "$current_body" && "$tasks_created" -lt "$MAX_TASKS" ]]; then
|
|
685
|
+
create_single_task "$current_file" "$current_body"
|
|
686
|
+
tasks_created=$((tasks_created + 1))
|
|
687
|
+
fi
|
|
688
|
+
|
|
689
|
+
if [[ "$tasks_created" -gt 0 ]]; then
|
|
690
|
+
log_success "Created $tasks_created kanban task(s) from log errors"
|
|
691
|
+
else
|
|
692
|
+
log_info "No new kanban tasks created."
|
|
693
|
+
fi
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
create_single_task() {
|
|
697
|
+
local file="$1"
|
|
698
|
+
local body="$2"
|
|
699
|
+
|
|
700
|
+
# Truncate body if too long (keep first 40 lines)
|
|
701
|
+
local line_count
|
|
702
|
+
line_count=$(echo "$body" | wc -l | xargs)
|
|
703
|
+
if [[ "$line_count" -gt 40 ]]; then
|
|
704
|
+
body=$(echo "$body" | head -40)
|
|
705
|
+
body="$body"$'\n'"... (truncated, $line_count total lines)"
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
# Extract the first actual error line for the task title
|
|
709
|
+
local error_line
|
|
710
|
+
error_line=$(echo "$body" | grep -m1 -iE \
|
|
711
|
+
'(error|exception|fatal|panic|critical|traceback|refused|denied|killed|segfault|ECONNREFUSED|ENOENT)' \
|
|
712
|
+
2>/dev/null || echo "Error detected in log")
|
|
713
|
+
error_line=$(echo "$error_line" | head -c 120) # cap length
|
|
714
|
+
|
|
715
|
+
local task_body="[Bug Report - Log Scanner]
|
|
716
|
+
File: $file
|
|
717
|
+
Error: $error_line
|
|
718
|
+
|
|
719
|
+
Context:
|
|
720
|
+
$body"
|
|
721
|
+
|
|
722
|
+
if [[ "$DRY_RUN" = "true" ]]; then
|
|
723
|
+
echo "" >&2
|
|
724
|
+
echo -e "${YELLOW}[DRY RUN] Would create task:${NC}" >&2
|
|
725
|
+
echo -e " File: $file" >&2
|
|
726
|
+
echo -e " Error: $error_line" >&2
|
|
727
|
+
echo -e " Context lines: $line_count" >&2
|
|
728
|
+
return 0
|
|
729
|
+
fi
|
|
730
|
+
|
|
731
|
+
log_info "Creating kanban task for error in $file..."
|
|
732
|
+
|
|
733
|
+
# Escape single quotes in the task body for shell safety
|
|
734
|
+
local escaped_body
|
|
735
|
+
escaped_body=$(printf '%s' "$task_body" | sed "s/'/'\\\\''/g")
|
|
736
|
+
|
|
737
|
+
# Create the kanban task
|
|
738
|
+
local result
|
|
739
|
+
if result=$("$KANBAN_SCRIPT" create "$task_body" --tags "log-scanner,bug-report" < /dev/null 2>&1); then
|
|
740
|
+
local task_id
|
|
741
|
+
task_id=$(echo "$result" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*: *"//' | sed 's/"$//' || echo "unknown")
|
|
742
|
+
log_success "Created task $task_id for: $error_line"
|
|
743
|
+
else
|
|
744
|
+
log_error "Failed to create kanban task: $result"
|
|
745
|
+
fi
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
# ── Main ─────────────────────────────────────────────────────────────────────
|
|
749
|
+
main() {
|
|
750
|
+
local scan_start
|
|
751
|
+
scan_start=$(now_iso)
|
|
752
|
+
|
|
753
|
+
log_info "=== Log Scanner v$VERSION ==="
|
|
754
|
+
log_info "Scan directory: $SCAN_DIR"
|
|
755
|
+
|
|
756
|
+
# Get last-checked timestamp
|
|
757
|
+
local last_checked
|
|
758
|
+
last_checked=$(get_last_checked)
|
|
759
|
+
|
|
760
|
+
# Create temp files for intermediate results
|
|
761
|
+
local tmp_dir
|
|
762
|
+
tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'log_scanner')
|
|
763
|
+
local raw_results="$tmp_dir/raw_results.txt"
|
|
764
|
+
local grouped_blocks="$tmp_dir/grouped_blocks.txt"
|
|
765
|
+
touch "$raw_results" "$grouped_blocks"
|
|
766
|
+
|
|
767
|
+
# Cleanup on exit
|
|
768
|
+
trap "rm -rf '$tmp_dir'" EXIT
|
|
769
|
+
|
|
770
|
+
# Phase 1: Scan log files
|
|
771
|
+
log_info "Phase 1: Scanning log files..."
|
|
772
|
+
scan_logs "$raw_results" "$last_checked"
|
|
773
|
+
|
|
774
|
+
# Phase 2: Deduplicate and group
|
|
775
|
+
log_info "Phase 2: Grouping results..."
|
|
776
|
+
deduplicate_results "$raw_results" "$grouped_blocks"
|
|
777
|
+
|
|
778
|
+
# Phase 3: Create kanban tasks (or dry-run)
|
|
779
|
+
log_info "Phase 3: Creating kanban tasks..."
|
|
780
|
+
create_kanban_tasks "$grouped_blocks"
|
|
781
|
+
|
|
782
|
+
# Save the scan timestamp (even in dry-run to avoid re-scanning on next real run)
|
|
783
|
+
if [[ "$DRY_RUN" != "true" ]]; then
|
|
784
|
+
save_last_checked "$scan_start"
|
|
785
|
+
fi
|
|
786
|
+
|
|
787
|
+
log_info "Scan complete."
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
main
|