juno-code 1.0.46 → 1.0.49

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 (54) hide show
  1. package/README.md +44 -8
  2. package/dist/bin/cli.d.mts +17 -0
  3. package/dist/bin/cli.d.ts +17 -0
  4. package/dist/bin/cli.js +5601 -17505
  5. package/dist/bin/cli.js.map +1 -1
  6. package/dist/bin/cli.mjs +5640 -17542
  7. package/dist/bin/cli.mjs.map +1 -1
  8. package/dist/bin/feedback-collector.d.mts +2 -0
  9. package/dist/bin/feedback-collector.d.ts +2 -0
  10. package/dist/bin/feedback-collector.js.map +1 -1
  11. package/dist/bin/feedback-collector.mjs.map +1 -1
  12. package/dist/index.d.mts +2107 -0
  13. package/dist/index.d.ts +2107 -0
  14. package/dist/index.js +3760 -14728
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +3761 -14536
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/templates/extensions/pi/juno-skill-preprocessor.ts +239 -0
  19. package/dist/templates/scripts/__pycache__/github.cpython-313.pyc +0 -0
  20. package/dist/templates/scripts/__pycache__/parallel_runner.cpython-313.pyc +0 -0
  21. package/dist/templates/scripts/__pycache__/slack_respond.cpython-313.pyc +0 -0
  22. package/dist/templates/scripts/kanban.sh +18 -4
  23. package/dist/templates/scripts/parallel_runner.sh +2242 -0
  24. package/dist/templates/services/README.md +61 -1
  25. package/dist/templates/services/__pycache__/claude.cpython-313.pyc +0 -0
  26. package/dist/templates/services/__pycache__/codex.cpython-313.pyc +0 -0
  27. package/dist/templates/services/__pycache__/pi.cpython-313.pyc +0 -0
  28. package/dist/templates/services/claude.py +132 -33
  29. package/dist/templates/services/codex.py +179 -66
  30. package/dist/templates/services/gemini.py +117 -27
  31. package/dist/templates/services/pi.py +1753 -0
  32. package/dist/templates/skills/claude/plan-kanban-tasks/SKILL.md +14 -7
  33. package/dist/templates/skills/claude/ralph-loop/SKILL.md +18 -22
  34. package/dist/templates/skills/claude/ralph-loop/references/first_check.md +15 -14
  35. package/dist/templates/skills/claude/ralph-loop/references/implement.md +17 -17
  36. package/dist/templates/skills/claude/ralph-loop/scripts/kanban.sh +18 -4
  37. package/dist/templates/skills/claude/understand-project/SKILL.md +14 -7
  38. package/dist/templates/skills/codex/ralph-loop/SKILL.md +18 -22
  39. package/dist/templates/skills/codex/ralph-loop/references/first_check.md +15 -14
  40. package/dist/templates/skills/codex/ralph-loop/references/implement.md +17 -17
  41. package/dist/templates/skills/codex/ralph-loop/scripts/kanban.sh +18 -4
  42. package/dist/templates/skills/pi/.gitkeep +0 -0
  43. package/dist/templates/skills/pi/plan-kanban-tasks/SKILL.md +32 -0
  44. package/dist/templates/skills/pi/ralph-loop/SKILL.md +39 -0
  45. package/dist/templates/skills/pi/ralph-loop/references/first_check.md +21 -0
  46. package/dist/templates/skills/pi/ralph-loop/references/implement.md +99 -0
  47. package/dist/templates/skills/pi/understand-project/SKILL.md +46 -0
  48. package/package.json +20 -42
  49. package/dist/templates/scripts/__pycache__/attachment_downloader.cpython-38.pyc +0 -0
  50. package/dist/templates/scripts/__pycache__/github.cpython-38.pyc +0 -0
  51. package/dist/templates/scripts/__pycache__/slack_fetch.cpython-38.pyc +0 -0
  52. package/dist/templates/scripts/__pycache__/slack_state.cpython-38.pyc +0 -0
  53. package/dist/templates/services/__pycache__/claude.cpython-38.pyc +0 -0
  54. package/dist/templates/services/__pycache__/codex.cpython-38.pyc +0 -0
@@ -9,6 +9,8 @@ import json
9
9
  import os
10
10
  import subprocess
11
11
  import sys
12
+ import threading
13
+ import time
12
14
  from datetime import datetime
13
15
  from typing import List, Optional, Tuple
14
16
 
@@ -351,9 +353,28 @@ Examples:
351
353
 
352
354
  def run_gemini(self, cmd: List[str], verbose: bool = False) -> int:
353
355
  """Execute the Gemini CLI and normalize streaming output for shell-backend consumption."""
356
+ capture_path = os.environ.get("JUNO_SUBAGENT_CAPTURE_PATH")
354
357
  if verbose:
355
- print(f"Executing: {' '.join(cmd)}", file=sys.stderr)
356
- print("-" * 80, file=sys.stderr)
358
+ # Truncate prompt in display to avoid confusing multi-line output
359
+ display_cmd = []
360
+ skip_next = False
361
+ for i, part in enumerate(cmd):
362
+ if skip_next:
363
+ skip_next = False
364
+ continue
365
+ if part == "-p" and i + 1 < len(cmd):
366
+ prompt_val = cmd[i + 1]
367
+ if len(prompt_val) > 80 or "\n" in prompt_val:
368
+ first_line = prompt_val.split("\n")[0][:60]
369
+ display_cmd.append(f'-p "{first_line}..." ({len(prompt_val)} chars)')
370
+ else:
371
+ display_cmd.append(f"-p {prompt_val}")
372
+ skip_next = True
373
+ else:
374
+ display_cmd.append(part)
375
+ if not capture_path:
376
+ print(f"Executing: {' '.join(display_cmd)}", file=sys.stderr)
377
+ print("-" * 80, file=sys.stderr)
357
378
 
358
379
  try:
359
380
  process = subprocess.Popen(
@@ -366,33 +387,92 @@ Examples:
366
387
  cwd=self.project_path,
367
388
  )
368
389
 
369
- pending = ""
370
-
371
- if process.stdout:
372
- for raw_line in process.stdout:
373
- combined = (pending + raw_line).replace("\x7f", "\n")
374
- pending = ""
375
-
376
- if not combined.strip():
377
- continue
390
+ # Watchdog thread: handles two scenarios where the stdout loop blocks:
391
+ # 1. Process exits but its stdout pipe stays open (inherited FDs)
392
+ # 2. Process itself never exits (hung event loop)
393
+ # The watchdog waits for an "output done" signal from the main thread
394
+ # (set when the stdout loop finishes). Once signaled, it gives the
395
+ # process a grace period to exit, then terminates it and closes stdout.
396
+ wait_timeout = int(os.environ.get("GEMINI_WAIT_TIMEOUT", "30"))
397
+ output_done = threading.Event()
398
+
399
+ def _stdout_watchdog():
400
+ """Terminate process and close stdout pipe if it hangs after output."""
401
+ # Wait until the main thread signals output is done,
402
+ # OR until the process exits on its own (poll every second).
403
+ while not output_done.is_set():
404
+ if process.poll() is not None:
405
+ break
406
+ output_done.wait(timeout=1)
407
+
408
+ if output_done.is_set() and process.poll() is None:
409
+ # Output is done but process hasn't exited — give it grace period.
410
+ try:
411
+ process.wait(timeout=wait_timeout)
412
+ except subprocess.TimeoutExpired:
413
+ print(
414
+ f"Warning: Gemini process did not exit within {wait_timeout}s after output. Terminating.",
415
+ file=sys.stderr
416
+ )
417
+ process.terminate()
418
+ try:
419
+ process.wait(timeout=5)
420
+ except subprocess.TimeoutExpired:
421
+ print("Warning: Gemini process did not respond to SIGTERM. Killing.", file=sys.stderr)
422
+ process.kill()
423
+ try:
424
+ process.wait(timeout=5)
425
+ except subprocess.TimeoutExpired:
426
+ pass
378
427
 
379
- parts, pending = self._split_json_stream(combined)
428
+ # Process has exited (naturally or forcefully).
429
+ # Grace period for remaining pipe data to flush, then close stdout.
430
+ time.sleep(2)
431
+ try:
432
+ if process.stdout and not process.stdout.closed:
433
+ process.stdout.close()
434
+ except Exception:
435
+ pass
380
436
 
381
- if parts:
382
- for part in parts:
383
- try:
384
- parsed = json.loads(part)
385
- formatted = self._format_event_pretty(parsed)
386
- print(formatted, flush=True)
387
- except Exception:
388
- print(part, flush=True)
389
- continue
437
+ watchdog = threading.Thread(target=_stdout_watchdog, daemon=True)
438
+ watchdog.start()
390
439
 
391
- # Fallback for non-JSON lines or partial content
392
- if pending:
393
- continue
440
+ pending = ""
394
441
 
395
- print(combined, end="" if combined.endswith("\n") else "\n", flush=True)
442
+ if process.stdout:
443
+ try:
444
+ for raw_line in process.stdout:
445
+ combined = (pending + raw_line).replace("\x7f", "\n")
446
+ pending = ""
447
+
448
+ if not combined.strip():
449
+ continue
450
+
451
+ parts, pending = self._split_json_stream(combined)
452
+
453
+ if parts:
454
+ for part in parts:
455
+ try:
456
+ parsed = json.loads(part)
457
+ formatted = self._format_event_pretty(parsed)
458
+ print(formatted, flush=True)
459
+ except Exception:
460
+ print(part, flush=True)
461
+ continue
462
+
463
+ # Fallback for non-JSON lines or partial content
464
+ if pending:
465
+ continue
466
+
467
+ print(combined, end="" if combined.endswith("\n") else "\n", flush=True)
468
+ except ValueError:
469
+ # Watchdog closed stdout because the process exited but the
470
+ # pipe stayed open (inherited FDs from child processes).
471
+ # This is expected — all output has been consumed.
472
+ pass
473
+
474
+ # Signal watchdog that output is done (stdout loop has exited).
475
+ output_done.set()
396
476
 
397
477
  if pending.strip():
398
478
  try:
@@ -401,7 +481,13 @@ Examples:
401
481
  except Exception:
402
482
  print(pending, flush=True)
403
483
 
404
- process.wait()
484
+ # Ensure process has exited. The watchdog thread handles termination
485
+ # if the process hangs, so by this point it should already be dead.
486
+ # Use a short timeout as a safety net.
487
+ try:
488
+ process.wait(timeout=5)
489
+ except subprocess.TimeoutExpired:
490
+ pass # Watchdog thread will handle cleanup
405
491
 
406
492
  if process.stderr and process.returncode != 0:
407
493
  stderr_output = process.stderr.read()
@@ -414,7 +500,11 @@ Examples:
414
500
  print("\nInterrupted by user", file=sys.stderr)
415
501
  try:
416
502
  process.terminate()
417
- process.wait()
503
+ try:
504
+ process.wait(timeout=5)
505
+ except subprocess.TimeoutExpired:
506
+ process.kill()
507
+ process.wait(timeout=5)
418
508
  except Exception:
419
509
  pass
420
510
  return 130