@synkro-sh/cli 1.1.6 → 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 +171 -107
- 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
|
-
across N gradings.
|
|
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.
|
|
1336
1340
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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).
|
|
1343
|
+
|
|
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 = 60
|
|
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,92 +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
|
+
|
|
1416
|
+
|
|
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
|
+
"""
|
|
1384
1427
|
def __init__(self, primer):
|
|
1385
1428
|
self.primer = primer or ""
|
|
1386
|
-
self.
|
|
1387
|
-
self.
|
|
1388
|
-
self.
|
|
1389
|
-
self.
|
|
1390
|
-
self.
|
|
1391
|
-
|
|
1392
|
-
def
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
],
|
|
1429
|
+
self._warm_proc = None
|
|
1430
|
+
self._warm_thread = None
|
|
1431
|
+
self._lock = threading.Lock()
|
|
1432
|
+
self._total_grades = 0
|
|
1433
|
+
self._start_prewarm()
|
|
1434
|
+
|
|
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]
|
|
1445
|
+
return subprocess.Popen(
|
|
1446
|
+
cmd,
|
|
1405
1447
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
1406
1448
|
stderr=subprocess.DEVNULL, text=True, bufsize=1,
|
|
1407
1449
|
)
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
def
|
|
1416
|
-
msg = json.dumps({
|
|
1417
|
-
"type": "user",
|
|
1418
|
-
"message": {"role": "user", "content": [{"type": "text", "text": text}]},
|
|
1419
|
-
"parent_tool_use_id": None,
|
|
1420
|
-
"session_id": "",
|
|
1421
|
-
})
|
|
1450
|
+
|
|
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):
|
|
1422
1458
|
try:
|
|
1423
|
-
|
|
1424
|
-
self.
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
return ""
|
|
1444
|
-
try:
|
|
1445
|
-
obj = json.loads(line)
|
|
1446
|
-
except json.JSONDecodeError:
|
|
1447
|
-
continue
|
|
1448
|
-
t = obj.get("type")
|
|
1449
|
-
if t == "assistant":
|
|
1450
|
-
for c in obj.get("message", {}).get("content", []):
|
|
1451
|
-
if c.get("type") == "text":
|
|
1452
|
-
acc.append(c["text"])
|
|
1453
|
-
elif t == "result":
|
|
1454
|
-
return "".join(acc)
|
|
1459
|
+
log("pre-warming process")
|
|
1460
|
+
proc = self._make_proc()
|
|
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)
|
|
1473
|
+
except Exception as e:
|
|
1474
|
+
log(f"pre-warm failed: {e}")
|
|
1475
|
+
|
|
1476
|
+
def _start_prewarm(self):
|
|
1477
|
+
self._warm_thread = threading.Thread(target=self._prewarm, daemon=True)
|
|
1478
|
+
self._warm_thread.start()
|
|
1455
1479
|
|
|
1456
1480
|
def grade(self, prompt):
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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()
|
|
1495
|
+
try:
|
|
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()
|
|
1509
|
+
return resp
|
|
1510
|
+
|
|
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
|
|
1469
1516
|
|
|
1470
1517
|
|
|
1471
1518
|
def serve(primer):
|
|
@@ -1479,7 +1526,7 @@ def serve(primer):
|
|
|
1479
1526
|
os.write(pid_fd, f"{os.getpid()}\\n".encode())
|
|
1480
1527
|
os.fsync(pid_fd)
|
|
1481
1528
|
|
|
1482
|
-
|
|
1529
|
+
grader = WarmGrader(primer)
|
|
1483
1530
|
|
|
1484
1531
|
if SOCK_PATH.exists():
|
|
1485
1532
|
SOCK_PATH.unlink()
|
|
@@ -1493,24 +1540,30 @@ def serve(primer):
|
|
|
1493
1540
|
except Exception: pass
|
|
1494
1541
|
try: PID_FILE.unlink()
|
|
1495
1542
|
except Exception: pass
|
|
1496
|
-
|
|
1497
|
-
except Exception: pass
|
|
1543
|
+
grader.shutdown()
|
|
1498
1544
|
sys.exit(0)
|
|
1499
1545
|
signal.signal(signal.SIGTERM, cleanup)
|
|
1500
1546
|
signal.signal(signal.SIGINT, cleanup)
|
|
1501
1547
|
|
|
1502
|
-
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}")
|
|
1503
1549
|
|
|
1550
|
+
last_activity = time.time()
|
|
1551
|
+
sock.settimeout(30)
|
|
1504
1552
|
while True:
|
|
1505
1553
|
try:
|
|
1506
1554
|
conn, _ = sock.accept()
|
|
1507
|
-
|
|
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()
|
|
1508
1561
|
except Exception as e:
|
|
1509
1562
|
log(f"accept error: {e}")
|
|
1510
1563
|
time.sleep(0.1)
|
|
1511
1564
|
|
|
1512
1565
|
|
|
1513
|
-
def _handle_conn(conn,
|
|
1566
|
+
def _handle_conn(conn, grader):
|
|
1514
1567
|
try:
|
|
1515
1568
|
with conn:
|
|
1516
1569
|
length_bytes = b""
|
|
@@ -1527,7 +1580,7 @@ def _handle_conn(conn, daemon):
|
|
|
1527
1580
|
chunk = conn.recv(min(65536, length - len(prompt)))
|
|
1528
1581
|
if not chunk: break
|
|
1529
1582
|
prompt += chunk
|
|
1530
|
-
response =
|
|
1583
|
+
response = grader.grade(prompt.decode("utf-8", errors="replace"))
|
|
1531
1584
|
resp_bytes = response.encode("utf-8")
|
|
1532
1585
|
conn.sendall(len(resp_bytes).to_bytes(8, "big"))
|
|
1533
1586
|
conn.sendall(resp_bytes)
|
|
@@ -1654,7 +1707,7 @@ JUDGING PRIORITY:
|
|
|
1654
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\`.
|
|
1655
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.
|
|
1656
1709
|
|
|
1657
|
-
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.
|
|
1658
1711
|
|
|
1659
1712
|
OUTPUT RULES \u2014 strictest possible, no exceptions:
|
|
1660
1713
|
|
|
@@ -2090,7 +2143,7 @@ function writeConfigEnv(opts) {
|
|
|
2090
2143
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2091
2144
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2092
2145
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2093
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.1.
|
|
2146
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.1.8")}`
|
|
2094
2147
|
];
|
|
2095
2148
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2096
2149
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -2215,6 +2268,17 @@ async function installCommand(opts = {}) {
|
|
|
2215
2268
|
console.log(` ${scripts.sessionStartScript}
|
|
2216
2269
|
`);
|
|
2217
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
|
+
}
|
|
2218
2282
|
console.log("Wrote local-tier grader daemon:");
|
|
2219
2283
|
console.log(` ${GRADER_DAEMON_PATH}`);
|
|
2220
2284
|
console.log(` ${GRADER_PRIMER_EDIT_PATH}`);
|