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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/cli.js +658 -50
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/bin/cli.mjs +658 -50
  5. package/dist/bin/cli.mjs.map +1 -1
  6. package/dist/index.js +6 -4
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +6 -4
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/templates/scripts/__pycache__/attachment_downloader.cpython-38.pyc +0 -0
  11. package/dist/templates/scripts/__pycache__/github.cpython-38.pyc +0 -0
  12. package/dist/templates/scripts/__pycache__/slack_fetch.cpython-38.pyc +0 -0
  13. package/dist/templates/scripts/__pycache__/slack_state.cpython-38.pyc +0 -0
  14. package/dist/templates/scripts/attachment_downloader.py +405 -0
  15. package/dist/templates/scripts/github.py +282 -7
  16. package/dist/templates/scripts/hooks/session_counter.sh +328 -0
  17. package/dist/templates/scripts/kanban.sh +22 -4
  18. package/dist/templates/scripts/log_scanner.sh +790 -0
  19. package/dist/templates/scripts/slack_fetch.py +232 -20
  20. package/dist/templates/services/claude.py +50 -1
  21. package/dist/templates/services/codex.py +5 -4
  22. package/dist/templates/skills/claude/.gitkeep +0 -0
  23. package/dist/templates/skills/claude/plan-kanban-tasks/SKILL.md +25 -0
  24. package/dist/templates/skills/claude/ralph-loop/SKILL.md +43 -0
  25. package/dist/templates/skills/claude/ralph-loop/references/first_check.md +20 -0
  26. package/dist/templates/skills/claude/ralph-loop/references/implement.md +99 -0
  27. package/dist/templates/skills/claude/ralph-loop/scripts/kanban.sh +293 -0
  28. package/dist/templates/skills/claude/understand-project/SKILL.md +39 -0
  29. package/dist/templates/skills/codex/.gitkeep +0 -0
  30. package/dist/templates/skills/codex/ralph-loop/SKILL.md +43 -0
  31. package/dist/templates/skills/codex/ralph-loop/references/first_check.md +20 -0
  32. package/dist/templates/skills/codex/ralph-loop/references/implement.md +99 -0
  33. package/dist/templates/skills/codex/ralph-loop/scripts/kanban.sh +293 -0
  34. 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