@synkro-sh/cli 1.1.7 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.js +161 -151
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -815,9 +815,12 @@ if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; t
|
|
|
815
815
|
# local CC inference that's unacceptable. Pre-stuffing the rules costs
|
|
816
816
|
# tokens but keeps grade latency in the 1-3s range. Bounded at 1.5s; on
|
|
817
817
|
# failure proceed with empty rules (degrades to baseline-only judging).
|
|
818
|
-
ORG_RULES=$(
|
|
818
|
+
ORG_RULES=$(printf '%s' "$PROPOSED" | head -c 8000 \\
|
|
819
|
+
| jq -Rs '{content: .}' \\
|
|
820
|
+
| curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
|
|
821
|
+
-X POST -H "Content-Type: application/json" \\
|
|
819
822
|
-H "Authorization: Bearer $JWT" \\
|
|
820
|
-
--max-time
|
|
823
|
+
-d @- --max-time 2 2>/dev/null \\
|
|
821
824
|
| jq -c '[.rules[]? | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
|
|
822
825
|
if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
|
|
823
826
|
|
|
@@ -1262,16 +1265,18 @@ if [ -z "$RESP" ]; then
|
|
|
1262
1265
|
exit 0
|
|
1263
1266
|
fi
|
|
1264
1267
|
|
|
1268
|
+
PLAN_NUDGE="Before implementing any multi-step plan, call the synkro-guardrails analyze_plan tool with your implementation plan to check for relevant org coding rules."
|
|
1269
|
+
|
|
1265
1270
|
OPEN=$(echo "$RESP" | jq -r '.open_count // 0' 2>/dev/null)
|
|
1266
1271
|
if [ "$OPEN" = "0" ] || [ -z "$OPEN" ]; then
|
|
1267
|
-
|
|
1272
|
+
jq -n --arg sys_msg "[synkro] $PLAN_NUDGE" '{ systemMessage: $sys_msg }'
|
|
1268
1273
|
exit 0
|
|
1269
1274
|
fi
|
|
1270
1275
|
|
|
1271
1276
|
if [ "$OPEN" = "1" ]; then
|
|
1272
|
-
SYS_MSG="[synkro] session start \u2192 1 open finding in this repo from a prior session"
|
|
1277
|
+
SYS_MSG="[synkro] session start \u2192 1 open finding in this repo from a prior session. $PLAN_NUDGE"
|
|
1273
1278
|
else
|
|
1274
|
-
SYS_MSG="[synkro] session start \u2192 \${OPEN} open findings in this repo from prior sessions"
|
|
1279
|
+
SYS_MSG="[synkro] session start \u2192 \${OPEN} open findings in this repo from prior sessions. $PLAN_NUDGE"
|
|
1275
1280
|
fi
|
|
1276
1281
|
|
|
1277
1282
|
jq -n --arg sys_msg "$SYS_MSG" '{ systemMessage: $sys_msg }'
|
|
@@ -1329,15 +1334,14 @@ var init_graderDaemon = __esm({
|
|
|
1329
1334
|
"use strict";
|
|
1330
1335
|
GRADER_DAEMON_PY = `#!/usr/bin/env python3
|
|
1331
1336
|
"""
|
|
1332
|
-
Synkro grader
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1337
|
+
Synkro warm-pool grader \u2014 pre-warmed \`claude --print --system-prompt\` process
|
|
1338
|
+
pool fronted by a Unix socket. Each grade uses one warm process and kills it;
|
|
1339
|
+
a replacement is pre-warmed in the background.
|
|
1340
|
+
|
|
1341
|
+
Zero context bloat: 1 grade per process, system prompt via --system-prompt flag
|
|
1342
|
+
(single inference call, no primer-as-conversation-turn overhead).
|
|
1336
1343
|
|
|
1337
|
-
|
|
1338
|
-
ROTATION_CALLS (default 10) gradings or ROTATION_AGE_SEC (default 1h),
|
|
1339
|
-
whichever comes first. Each rotation eats a one-time ~5s primer cost; calls
|
|
1340
|
-
in between target ~2-3s steady-state.
|
|
1344
|
+
Warm steady-state: ~2-3s per grade. Cold fallback: ~5-6s if pre-warm not ready.
|
|
1341
1345
|
|
|
1342
1346
|
Commands:
|
|
1343
1347
|
start [primer-path] - bring up daemon if not running
|
|
@@ -1346,13 +1350,10 @@ Commands:
|
|
|
1346
1350
|
status - print "running"/"stopped"
|
|
1347
1351
|
"""
|
|
1348
1352
|
|
|
1349
|
-
import os, sys, json, socket, time,
|
|
1353
|
+
import os, sys, json, socket, time, signal, fcntl, re
|
|
1350
1354
|
import subprocess, threading
|
|
1351
1355
|
from pathlib import Path
|
|
1352
1356
|
|
|
1353
|
-
# Each "mode" gets its own daemon process: separate socket, pid, log.
|
|
1354
|
-
# Modes: "edit" (precheck + post-edit, schema {ok, severity, ...}) and "bash"
|
|
1355
|
-
# (schema {verdict, severity, ...}). Selected via --mode <name>; default "edit".
|
|
1356
1357
|
ALLOWED_MODE_RE = re.compile(r"^[a-z][a-z0-9_-]{0,30}$")
|
|
1357
1358
|
DAEMON_BASE = Path.home() / ".synkro" / "daemon"
|
|
1358
1359
|
DAEMON_BASE.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
@@ -1365,11 +1366,10 @@ def mode_paths(mode):
|
|
|
1365
1366
|
MODE = "edit"
|
|
1366
1367
|
PID_FILE, SOCK_PATH, LOG_FILE = mode_paths(MODE)
|
|
1367
1368
|
|
|
1368
|
-
|
|
1369
|
-
ROTATION_AGE_SEC = int(os.environ.get("SYNKRO_DAEMON_ROTATE_AGE", "3600"))
|
|
1370
|
-
GRADE_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_GRADE_TIMEOUT", "10"))
|
|
1369
|
+
GRADE_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_GRADE_TIMEOUT", "45"))
|
|
1371
1370
|
DEFAULT_MODEL = os.environ.get("SYNKRO_DAEMON_MODEL", "claude-sonnet-4-6")
|
|
1372
1371
|
MAX_PROMPT_BYTES = 4 * 1024 * 1024
|
|
1372
|
+
IDLE_SHUTDOWN_SEC = int(os.environ.get("SYNKRO_DAEMON_IDLE_TIMEOUT", "600"))
|
|
1373
1373
|
|
|
1374
1374
|
|
|
1375
1375
|
def log(msg):
|
|
@@ -1380,146 +1380,139 @@ def log(msg):
|
|
|
1380
1380
|
pass
|
|
1381
1381
|
|
|
1382
1382
|
|
|
1383
|
-
|
|
1383
|
+
def _read_response(proc, timeout=45):
|
|
1384
|
+
acc = []
|
|
1385
|
+
deadline = time.time() + timeout
|
|
1386
|
+
while True:
|
|
1387
|
+
if time.time() > deadline:
|
|
1388
|
+
log("read timeout")
|
|
1389
|
+
return ""
|
|
1390
|
+
line = proc.stdout.readline()
|
|
1391
|
+
if not line:
|
|
1392
|
+
return ""
|
|
1393
|
+
try:
|
|
1394
|
+
obj = json.loads(line)
|
|
1395
|
+
except json.JSONDecodeError:
|
|
1396
|
+
continue
|
|
1397
|
+
t = obj.get("type")
|
|
1398
|
+
if t == "assistant":
|
|
1399
|
+
for c in obj.get("message", {}).get("content", []):
|
|
1400
|
+
if c.get("type") == "text":
|
|
1401
|
+
acc.append(c["text"])
|
|
1402
|
+
elif t == "result":
|
|
1403
|
+
return "".join(acc)
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def _send_msg(proc, text):
|
|
1407
|
+
msg = json.dumps({
|
|
1408
|
+
"type": "user",
|
|
1409
|
+
"message": {"role": "user", "content": [{"type": "text", "text": text}]},
|
|
1410
|
+
"parent_tool_use_id": None,
|
|
1411
|
+
"session_id": "",
|
|
1412
|
+
})
|
|
1413
|
+
proc.stdin.write(msg + "\\n")
|
|
1414
|
+
proc.stdin.flush()
|
|
1415
|
+
|
|
1384
1416
|
|
|
1385
|
-
class
|
|
1417
|
+
class WarmGrader:
|
|
1418
|
+
"""
|
|
1419
|
+
Keeps one pre-warmed claude process ready. Each grade pulls the warm
|
|
1420
|
+
process, sends one prompt, reads the verdict, kills the process, and
|
|
1421
|
+
starts pre-warming a replacement in the background.
|
|
1422
|
+
|
|
1423
|
+
The warm process has the system prompt loaded via --system-prompt and
|
|
1424
|
+
its KV cache primed by a warmup turn. The actual grade is a single
|
|
1425
|
+
inference call that benefits from the cached system prompt tokens.
|
|
1426
|
+
"""
|
|
1386
1427
|
def __init__(self, primer):
|
|
1387
1428
|
self.primer = primer or ""
|
|
1388
|
-
self.
|
|
1389
|
-
self.
|
|
1390
|
-
self.
|
|
1391
|
-
self.
|
|
1392
|
-
self.
|
|
1393
|
-
self._prewarm_thread = None
|
|
1394
|
-
self._spawn()
|
|
1429
|
+
self._warm_proc = None
|
|
1430
|
+
self._warm_thread = None
|
|
1431
|
+
self._lock = threading.Lock()
|
|
1432
|
+
self._total_grades = 0
|
|
1433
|
+
self._start_prewarm()
|
|
1395
1434
|
|
|
1396
1435
|
def _make_proc(self):
|
|
1436
|
+
cmd = [
|
|
1437
|
+
"claude", "--print", "--model", DEFAULT_MODEL,
|
|
1438
|
+
"--input-format=stream-json",
|
|
1439
|
+
"--output-format=stream-json",
|
|
1440
|
+
"--verbose",
|
|
1441
|
+
"--no-session-persistence",
|
|
1442
|
+
]
|
|
1443
|
+
if self.primer:
|
|
1444
|
+
cmd += ["--system-prompt", self.primer]
|
|
1397
1445
|
return subprocess.Popen(
|
|
1398
|
-
|
|
1399
|
-
"claude", "--print", "--model", DEFAULT_MODEL,
|
|
1400
|
-
"--input-format=stream-json",
|
|
1401
|
-
"--output-format=stream-json",
|
|
1402
|
-
"--verbose",
|
|
1403
|
-
"--no-session-persistence",
|
|
1404
|
-
],
|
|
1446
|
+
cmd,
|
|
1405
1447
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
1406
1448
|
stderr=subprocess.DEVNULL, text=True, bufsize=1,
|
|
1407
1449
|
)
|
|
1408
1450
|
|
|
1409
|
-
def
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
proc.stdin.write(msg + "\\n")
|
|
1417
|
-
proc.stdin.flush()
|
|
1418
|
-
|
|
1419
|
-
def _recv_from(self, proc):
|
|
1420
|
-
acc = []
|
|
1421
|
-
deadline = time.time() + GRADE_TIMEOUT_SEC
|
|
1422
|
-
while True:
|
|
1423
|
-
if time.time() > deadline:
|
|
1424
|
-
log("recv timeout")
|
|
1425
|
-
return ""
|
|
1426
|
-
line = proc.stdout.readline()
|
|
1427
|
-
if not line:
|
|
1428
|
-
return ""
|
|
1429
|
-
try:
|
|
1430
|
-
obj = json.loads(line)
|
|
1431
|
-
except json.JSONDecodeError:
|
|
1432
|
-
continue
|
|
1433
|
-
t = obj.get("type")
|
|
1434
|
-
if t == "assistant":
|
|
1435
|
-
for c in obj.get("message", {}).get("content", []):
|
|
1436
|
-
if c.get("type") == "text":
|
|
1437
|
-
acc.append(c["text"])
|
|
1438
|
-
elif t == "result":
|
|
1439
|
-
return "".join(acc)
|
|
1440
|
-
|
|
1441
|
-
def _spawn(self):
|
|
1442
|
-
if self.proc and self.proc.poll() is None:
|
|
1443
|
-
try: self.proc.terminate(); self.proc.wait(timeout=3)
|
|
1444
|
-
except Exception: self.proc.kill()
|
|
1445
|
-
log("spawning claude subprocess")
|
|
1446
|
-
self.proc = self._make_proc()
|
|
1447
|
-
if self.primer:
|
|
1448
|
-
self._send_to(self.proc, self.primer)
|
|
1449
|
-
primer_resp = self._recv_from(self.proc)
|
|
1450
|
-
log(f"primer ack: {primer_resp[:80]!r}")
|
|
1451
|
-
self.calls = 0
|
|
1452
|
-
self.start_time = time.time()
|
|
1453
|
-
self._next_proc = None
|
|
1454
|
-
|
|
1455
|
-
def _prewarm_worker(self):
|
|
1451
|
+
def _kill_proc(self, proc):
|
|
1452
|
+
try: proc.stdin.close()
|
|
1453
|
+
except Exception: pass
|
|
1454
|
+
try: proc.kill(); proc.wait(timeout=2)
|
|
1455
|
+
except Exception: pass
|
|
1456
|
+
|
|
1457
|
+
def _prewarm(self):
|
|
1456
1458
|
try:
|
|
1457
|
-
log("pre-warming
|
|
1459
|
+
log("pre-warming process")
|
|
1458
1460
|
proc = self._make_proc()
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1461
|
+
_send_msg(proc, "Ready")
|
|
1462
|
+
resp = _read_response(proc, timeout=30)
|
|
1463
|
+
if resp:
|
|
1464
|
+
with self._lock:
|
|
1465
|
+
old = self._warm_proc
|
|
1466
|
+
self._warm_proc = proc
|
|
1467
|
+
if old:
|
|
1468
|
+
self._kill_proc(old)
|
|
1469
|
+
log(f"pre-warm ready ({len(resp)} chars)")
|
|
1470
|
+
else:
|
|
1471
|
+
log("pre-warm response empty")
|
|
1472
|
+
self._kill_proc(proc)
|
|
1465
1473
|
except Exception as e:
|
|
1466
1474
|
log(f"pre-warm failed: {e}")
|
|
1467
|
-
self._next_proc = None
|
|
1468
|
-
|
|
1469
|
-
def _maybe_prewarm(self):
|
|
1470
|
-
if self._prewarm_thread and self._prewarm_thread.is_alive():
|
|
1471
|
-
return
|
|
1472
|
-
if self._next_proc is not None:
|
|
1473
|
-
return
|
|
1474
|
-
if self.calls >= ROTATION_CALLS - PREWARM_HEADROOM:
|
|
1475
|
-
self._prewarm_thread = threading.Thread(target=self._prewarm_worker, daemon=True)
|
|
1476
|
-
self._prewarm_thread.start()
|
|
1477
|
-
|
|
1478
|
-
def _try_rotate(self):
|
|
1479
|
-
if self._next_proc and self._next_proc.poll() is None:
|
|
1480
|
-
log("hot-swapping to pre-warmed subprocess")
|
|
1481
|
-
old = self.proc
|
|
1482
|
-
self.proc = self._next_proc
|
|
1483
|
-
self._next_proc = None
|
|
1484
|
-
self._prewarm_thread = None
|
|
1485
|
-
self.calls = 0
|
|
1486
|
-
self.start_time = time.time()
|
|
1487
|
-
if old and old.poll() is None:
|
|
1488
|
-
threading.Thread(target=lambda: (old.terminate(), old.wait()), daemon=True).start()
|
|
1489
|
-
return True
|
|
1490
|
-
log("pre-warm not ready, deferring rotation")
|
|
1491
|
-
return False
|
|
1492
1475
|
|
|
1493
|
-
def
|
|
1476
|
+
def _start_prewarm(self):
|
|
1477
|
+
self._warm_thread = threading.Thread(target=self._prewarm, daemon=True)
|
|
1478
|
+
self._warm_thread.start()
|
|
1479
|
+
|
|
1480
|
+
def grade(self, prompt):
|
|
1481
|
+
if self._warm_thread:
|
|
1482
|
+
self._warm_thread.join(timeout=60)
|
|
1483
|
+
|
|
1484
|
+
with self._lock:
|
|
1485
|
+
proc = self._warm_proc
|
|
1486
|
+
self._warm_proc = None
|
|
1487
|
+
|
|
1488
|
+
warm = True
|
|
1489
|
+
if not proc or proc.poll() is not None:
|
|
1490
|
+
log("no warm process, cold fallback")
|
|
1491
|
+
proc = self._make_proc()
|
|
1492
|
+
warm = False
|
|
1493
|
+
|
|
1494
|
+
t0 = time.time()
|
|
1494
1495
|
try:
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1496
|
+
_send_msg(proc, prompt)
|
|
1497
|
+
resp = _read_response(proc, timeout=GRADE_TIMEOUT_SEC)
|
|
1498
|
+
except Exception as e:
|
|
1499
|
+
log(f"grade error: {e}")
|
|
1500
|
+
resp = ""
|
|
1501
|
+
finally:
|
|
1502
|
+
self._kill_proc(proc)
|
|
1503
|
+
|
|
1504
|
+
elapsed = (time.time() - t0) * 1000
|
|
1505
|
+
self._total_grades += 1
|
|
1506
|
+
log(f"grade #{self._total_grades} {'warm' if warm else 'cold'} elapsed={elapsed:.0f}ms resp={len(resp)}ch")
|
|
1507
|
+
|
|
1508
|
+
self._start_prewarm()
|
|
1506
1509
|
return resp
|
|
1507
1510
|
|
|
1508
|
-
def
|
|
1509
|
-
with self.
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
elapsed = (time.time() - t0) * 1000
|
|
1514
|
-
self.calls += 1
|
|
1515
|
-
log(f"grade #{self.calls} elapsed={elapsed:.0f}ms resp_chars={len(resp)}")
|
|
1516
|
-
age = time.time() - self.start_time
|
|
1517
|
-
if self.calls >= ROTATION_CALLS or age >= ROTATION_AGE_SEC:
|
|
1518
|
-
if not self._try_rotate():
|
|
1519
|
-
self._maybe_prewarm()
|
|
1520
|
-
else:
|
|
1521
|
-
self._maybe_prewarm()
|
|
1522
|
-
return resp
|
|
1511
|
+
def shutdown(self):
|
|
1512
|
+
with self._lock:
|
|
1513
|
+
if self._warm_proc:
|
|
1514
|
+
self._kill_proc(self._warm_proc)
|
|
1515
|
+
self._warm_proc = None
|
|
1523
1516
|
|
|
1524
1517
|
|
|
1525
1518
|
def serve(primer):
|
|
@@ -1533,7 +1526,7 @@ def serve(primer):
|
|
|
1533
1526
|
os.write(pid_fd, f"{os.getpid()}\\n".encode())
|
|
1534
1527
|
os.fsync(pid_fd)
|
|
1535
1528
|
|
|
1536
|
-
|
|
1529
|
+
grader = WarmGrader(primer)
|
|
1537
1530
|
|
|
1538
1531
|
if SOCK_PATH.exists():
|
|
1539
1532
|
SOCK_PATH.unlink()
|
|
@@ -1547,24 +1540,30 @@ def serve(primer):
|
|
|
1547
1540
|
except Exception: pass
|
|
1548
1541
|
try: PID_FILE.unlink()
|
|
1549
1542
|
except Exception: pass
|
|
1550
|
-
|
|
1551
|
-
except Exception: pass
|
|
1543
|
+
grader.shutdown()
|
|
1552
1544
|
sys.exit(0)
|
|
1553
1545
|
signal.signal(signal.SIGTERM, cleanup)
|
|
1554
1546
|
signal.signal(signal.SIGINT, cleanup)
|
|
1555
1547
|
|
|
1556
|
-
log(f"daemon ready model={DEFAULT_MODEL} sock={SOCK_PATH}")
|
|
1548
|
+
log(f"daemon ready model={DEFAULT_MODEL} idle_shutdown={IDLE_SHUTDOWN_SEC}s sock={SOCK_PATH}")
|
|
1557
1549
|
|
|
1550
|
+
last_activity = time.time()
|
|
1551
|
+
sock.settimeout(30)
|
|
1558
1552
|
while True:
|
|
1559
1553
|
try:
|
|
1560
1554
|
conn, _ = sock.accept()
|
|
1561
|
-
|
|
1555
|
+
last_activity = time.time()
|
|
1556
|
+
threading.Thread(target=_handle_conn, args=(conn, grader), daemon=True).start()
|
|
1557
|
+
except socket.timeout:
|
|
1558
|
+
if time.time() - last_activity > IDLE_SHUTDOWN_SEC:
|
|
1559
|
+
log(f"idle for {IDLE_SHUTDOWN_SEC}s, shutting down")
|
|
1560
|
+
cleanup()
|
|
1562
1561
|
except Exception as e:
|
|
1563
1562
|
log(f"accept error: {e}")
|
|
1564
1563
|
time.sleep(0.1)
|
|
1565
1564
|
|
|
1566
1565
|
|
|
1567
|
-
def _handle_conn(conn,
|
|
1566
|
+
def _handle_conn(conn, grader):
|
|
1568
1567
|
try:
|
|
1569
1568
|
with conn:
|
|
1570
1569
|
length_bytes = b""
|
|
@@ -1581,7 +1580,7 @@ def _handle_conn(conn, daemon):
|
|
|
1581
1580
|
chunk = conn.recv(min(65536, length - len(prompt)))
|
|
1582
1581
|
if not chunk: break
|
|
1583
1582
|
prompt += chunk
|
|
1584
|
-
response =
|
|
1583
|
+
response = grader.grade(prompt.decode("utf-8", errors="replace"))
|
|
1585
1584
|
resp_bytes = response.encode("utf-8")
|
|
1586
1585
|
conn.sendall(len(resp_bytes).to_bytes(8, "big"))
|
|
1587
1586
|
conn.sendall(resp_bytes)
|
|
@@ -1708,7 +1707,7 @@ JUDGING PRIORITY:
|
|
|
1708
1707
|
2. BASELINE security issues \u2014 hardcoded real-looking secrets, eval/exec on user input, SQL string concat with untrusted input, MD5/SHA1 for security-sensitive purposes, unsafe deserialization, command injection, path traversal, missing auth on routes that mutate user/billing data, weak random for tokens, broken JWT verification, CORS misconfig, env-dump logging. Flag these even if no org rule covers them \u2014 they're universally bad. Use a sensible snake_case rule_id like \`no-hardcoded-secrets\`, \`eval-on-user-input\`, \`sql-string-concat\`.
|
|
1709
1708
|
3. Stylistic issues, placeholder fixtures, test files (path under /tests/, /__tests__/, *.test.*), and config-only files are NOT security issues \u2014 return ok=true.
|
|
1710
1709
|
|
|
1711
|
-
INDEPENDENCE: Each grade request is INDEPENDENT.
|
|
1710
|
+
INDEPENDENCE: Each grade request is INDEPENDENT. Judge ONLY the current request's File / User intent / Org rules / Diff. Prior "allows" do NOT authorize the current request \u2014 re-evaluate fresh against the rules in THIS prompt.
|
|
1712
1711
|
|
|
1713
1712
|
OUTPUT RULES \u2014 strictest possible, no exceptions:
|
|
1714
1713
|
|
|
@@ -2144,7 +2143,7 @@ function writeConfigEnv(opts) {
|
|
|
2144
2143
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2145
2144
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2146
2145
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2147
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.1.
|
|
2146
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.1.8")}`
|
|
2148
2147
|
];
|
|
2149
2148
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2150
2149
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -2269,6 +2268,17 @@ async function installCommand(opts = {}) {
|
|
|
2269
2268
|
console.log(` ${scripts.sessionStartScript}
|
|
2270
2269
|
`);
|
|
2271
2270
|
writeGraderDaemon();
|
|
2271
|
+
for (const mode of ["edit", "bash"]) {
|
|
2272
|
+
const pidFile = join4(SYNKRO_DIR, "daemon", mode, "daemon.pid");
|
|
2273
|
+
try {
|
|
2274
|
+
const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
|
|
2275
|
+
if (pid > 0) {
|
|
2276
|
+
process.kill(pid, "SIGTERM");
|
|
2277
|
+
console.log(`Stopped stale ${mode} daemon (pid ${pid})`);
|
|
2278
|
+
}
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2272
2282
|
console.log("Wrote local-tier grader daemon:");
|
|
2273
2283
|
console.log(` ${GRADER_DAEMON_PATH}`);
|
|
2274
2284
|
console.log(` ${GRADER_PRIMER_EDIT_PATH}`);
|