@synkro-sh/cli 1.1.8 → 1.2.1

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 CHANGED
@@ -301,6 +301,8 @@ var init_hookScripts = __esm({
301
301
  # Auth: reads access_token from ~/.synkro/credentials.json, sends Authorization: Bearer.
302
302
  set -e
303
303
 
304
+ synkro_log() { echo "[synkro] $1" >&2; }
305
+
304
306
  # Load config
305
307
  CONFIG_FILE="$HOME/.synkro/config.env"
306
308
  if [ -f "$CONFIG_FILE" ]; then
@@ -315,11 +317,13 @@ CREDS_PATH="\${SYNKRO_CREDENTIALS_PATH:-$HOME/.synkro/credentials.json}"
315
317
 
316
318
  # Fail open if not authed
317
319
  if [ ! -f "$CREDS_PATH" ]; then
320
+
318
321
  echo '{}'
319
322
  exit 0
320
323
  fi
321
324
  JWT=$(jq -r '.access_token // empty' "$CREDS_PATH" 2>/dev/null)
322
325
  if [ -z "$JWT" ]; then
326
+
323
327
  echo '{}'
324
328
  exit 0
325
329
  fi
@@ -344,6 +348,9 @@ if [ -z "$COMMAND" ]; then
344
348
  exit 0
345
349
  fi
346
350
 
351
+ CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
352
+ synkro_log "bashGuard checking: $CMD_SHORT"
353
+
347
354
  # NO hard regex gate \u2014 server-side bashShapes runs the universal pattern set
348
355
  # (including HTTP-payload destructive shapes like graphql_destructive_mutation
349
356
  # and http_method_delete) and returns a "trivial" fast-path verdict for boring
@@ -357,6 +364,14 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
357
364
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
358
365
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
359
366
  TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
367
+ # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
368
+ GIT_REPO=""
369
+ if command -v git >/dev/null 2>&1; then
370
+ _REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
371
+ if [ -n "$_REMOTE" ]; then
372
+ GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
373
+ fi
374
+ fi
360
375
  # Headless detection \u2014 when no human is in the loop, ASK is a no-op so we
361
376
  # fail-closed by upgrading high-tier findings to deny.
362
377
  PERMISSION_MODE=$(echo "$PAYLOAD" | jq -r '.permission_mode // empty' 2>/dev/null)
@@ -404,6 +419,7 @@ BODY=$(jq -n \\
404
419
  --arg session_id "$SESSION_ID" \\
405
420
  --arg tool_use_id "$TOOL_USE_ID" \\
406
421
  --arg cwd "$CWD" \\
422
+ --arg repo "$GIT_REPO" \\
407
423
  '{
408
424
  kind: "bash_judge",
409
425
  tool_input: $tool_input,
@@ -412,7 +428,8 @@ BODY=$(jq -n \\
412
428
  recent_actions: $recent_actions,
413
429
  session_id: (if ($session_id | length) > 0 then $session_id else null end),
414
430
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
415
- cwd: (if ($cwd | length) > 0 then $cwd else null end)
431
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
432
+ repo: (if ($repo | length) > 0 then $repo else null end)
416
433
  }')
417
434
 
418
435
  # Helper: refresh JWT via /api/auth/refresh and rewrite credentials.json.
@@ -464,28 +481,45 @@ if [ -z "$SYNKRO_INFERENCE_TIER" ]; then
464
481
  fi
465
482
  SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-free}"
466
483
 
484
+
467
485
  if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
468
486
  # \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (mode=bash). \u2500\u2500\u2500
487
+
488
+ # Fetch org guardrail rules relevant to this command (same as edit hook).
489
+ ORG_RULES=$(printf '%s' "$COMMAND" | head -c 4000 \\
490
+ | jq -Rs '{content: .}' \\
491
+ | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=15" \\
492
+ -X POST -H "Content-Type: application/json" \\
493
+ -H "Authorization: Bearer $JWT" \\
494
+ -d @- --max-time 2 2>/dev/null \\
495
+ | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
496
+ if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
497
+
469
498
  GRADER_PROMPT_FILE=$(mktemp -t synkro-bash-prompt.XXXXXX)
470
499
  trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
471
500
  printf 'Proposed command: %s\\n' "$COMMAND" > "$GRADER_PROMPT_FILE"
472
501
  printf 'User intent: %s\\n' "\${USER_INTENT:-none stated}" >> "$GRADER_PROMPT_FILE"
473
502
  printf 'Recent user messages: %s\\n' "$RECENT_USER_MESSAGES" >> "$GRADER_PROMPT_FILE"
474
503
  printf 'Recent actions: %s\\n' "$RECENT_ACTIONS" >> "$GRADER_PROMPT_FILE"
504
+ printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
475
505
 
476
506
  if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && [ -f "$HOME/.synkro/grader-primer-bash.txt" ] && command -v python3 >/dev/null 2>&1; then
507
+
477
508
  CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode bash grade "$HOME/.synkro/grader-primer-bash.txt" < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
478
509
  else
510
+
479
511
  CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
480
512
  fi
481
513
  V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | grep -oE '<synkro-verdict>[^<]*</synkro-verdict>' | tail -1 | sed -E 's|^<synkro-verdict>||; s|</synkro-verdict>$||')
482
514
  if [ -z "$V_INNER" ] || ! echo "$V_INNER" | jq -e '.severity' >/dev/null 2>&1; then
483
- echo '{}'
515
+ synkro_log "bashGuard $CMD_SHORT \u2192 error (no verdict)"
516
+ jq -n --arg m "[synkro] bashGuard \u2192 error (no verdict)" '{systemMessage: $m}'
484
517
  exit 0
485
518
  fi
486
519
  VERDICT="$V_INNER"
487
520
  else
488
521
  # \u2500\u2500\u2500 FAST TIER: server-side Cerebras grading. \u2500\u2500\u2500
522
+
489
523
  VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
490
524
  -H "Content-Type: application/json" \\
491
525
  -H "Authorization: Bearer $JWT" \\
@@ -493,6 +527,7 @@ else
493
527
  --max-time 6 2>/dev/null || echo "")
494
528
 
495
529
  if echo "$VERDICT" | grep -qE '"detail":"Token has expired|"detail":"Invalid or expired token'; then
530
+
496
531
  if refresh_jwt; then
497
532
  VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
498
533
  -H "Content-Type: application/json" \\
@@ -504,7 +539,8 @@ else
504
539
  fi
505
540
 
506
541
  if [ -z "$VERDICT" ]; then
507
- echo '{}'
542
+ synkro_log "bashGuard $CMD_SHORT \u2192 error (timeout)"
543
+ jq -n --arg m "[synkro] bashGuard \u2192 error (timeout)" '{systemMessage: $m}'
508
544
  exit 0
509
545
  fi
510
546
 
@@ -529,6 +565,7 @@ CATEGORY=$(echo "$VERDICT" | jq -r '.category // "destructive_command"' 2>/dev/n
529
565
 
530
566
  case "$SEVERITY" in
531
567
  critical)
568
+ synkro_log "bashGuard $CMD_SHORT \u2192 BLOCKED ($CATEGORY): $REASONING"
532
569
  SYS_MSG="[synkro] bashGuard \u2192 critical: \${REASONING}"
533
570
  if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
534
571
  SYS_MSG="\${SYS_MSG}\\n[synkro] suggested \u2192 \${ALTERNATIVE}"
@@ -553,6 +590,7 @@ case "$SEVERITY" in
553
590
  }'
554
591
  ;;
555
592
  high)
593
+ synkro_log "bashGuard $CMD_SHORT \u2192 FLAGGED ($CATEGORY): $REASONING"
556
594
  SYS_MSG="[synkro] bashGuard \u2192 high: \${REASONING}"
557
595
  if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
558
596
  SYS_MSG="\${SYS_MSG}\\n[synkro] suggested \u2192 \${ALTERNATIVE}"
@@ -583,8 +621,9 @@ case "$SEVERITY" in
583
621
  }'
584
622
  ;;
585
623
  *)
586
- # low / medium / anything else \u2192 silent allow
587
- echo '{}'
624
+ # low / medium / anything else \u2192 allow
625
+ synkro_log "bashGuard $CMD_SHORT \u2192 pass"
626
+ jq -n --arg m "[synkro] bashGuard \u2192 pass" '{systemMessage: $m}'
588
627
  ;;
589
628
  esac
590
629
 
@@ -605,6 +644,8 @@ exit 0
605
644
  # Always exits 0 with valid JSON. Fails open on any error.
606
645
  set -e
607
646
 
647
+ synkro_log() { echo "[synkro] $1" >&2; }
648
+
608
649
  CONFIG_FILE="$HOME/.synkro/config.env"
609
650
  if [ -f "$CONFIG_FILE" ]; then
610
651
  set -a
@@ -643,6 +684,14 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
643
684
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
644
685
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
645
686
  TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
687
+ # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
688
+ GIT_REPO=""
689
+ if command -v git >/dev/null 2>&1; then
690
+ _REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
691
+ if [ -n "$_REMOTE" ]; then
692
+ GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
693
+ fi
694
+ fi
646
695
  # Headless / non-interactive detection \u2014 when CC won't actually prompt the
647
696
  # human, our "ask" verdict is a no-op. Server uses these to fall back to
648
697
  # "deny" so we fail-closed instead of silently letting findings through.
@@ -655,6 +704,9 @@ if [ -z "$FILE_PATH" ]; then
655
704
  exit 0
656
705
  fi
657
706
 
707
+ FILE_SHORT=$(basename "$FILE_PATH")
708
+ synkro_log "editGuard checking: $FILE_SHORT"
709
+
658
710
  # Pull conversation context from the transcript file. CC writes one JSON line
659
711
  # per message; we read the tail and extract the most recent user message + the
660
712
  # last 5 tool_use blocks. The server uses these as anchors for cosine ranking
@@ -758,6 +810,7 @@ BODY=$(jq -n \\
758
810
  --arg cwd "$CWD" \\
759
811
  --arg permission_mode "$PERMISSION_MODE" \\
760
812
  --arg headless_flag "$HEADLESS_FLAG" \\
813
+ --arg repo "$GIT_REPO" \\
761
814
  '{
762
815
  file_path: $file_path,
763
816
  tool_name: $tool_name,
@@ -770,7 +823,8 @@ BODY=$(jq -n \\
770
823
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
771
824
  cwd: (if ($cwd | length) > 0 then $cwd else null end),
772
825
  permission_mode: (if ($permission_mode | length) > 0 then $permission_mode else null end),
773
- headless: ($headless_flag == "1")
826
+ headless: ($headless_flag == "1"),
827
+ repo: (if ($repo | length) > 0 then $repo else null end)
774
828
  }')
775
829
 
776
830
  # Refresh JWT on 401 (mirrors the bash judge pattern).
@@ -799,22 +853,9 @@ refresh_jwt() {
799
853
  return 0
800
854
  }
801
855
 
856
+
802
857
  if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
803
858
  # \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (Python helper).
804
- # The daemon at $HOME/.synkro/bin/grader_daemon.py keeps one
805
- # \`claude --print --input-format=stream-json\` process alive, primed once
806
- # with the role/format. Hook ships a thin grading prompt over Unix socket
807
- # and gets the verdict text back. Steady-state ~1.5\u20133s per grading vs
808
- # ~14s for cold \`claude --print\`. Falls back to direct \`claude --print\`
809
- # if the daemon binary or primer is missing.
810
-
811
- # Fetch the caller's visible org rules and inject into the grader prompt.
812
- # On-demand MCP retrieval inside the grader was the cleaner architecture
813
- # but added 20+ seconds of latency per grade because the model has to
814
- # call get_guardrails, wait for cosine ranking, then reason. For free-tier
815
- # local CC inference that's unacceptable. Pre-stuffing the rules costs
816
- # tokens but keeps grade latency in the 1-3s range. Bounded at 1.5s; on
817
- # failure proceed with empty rules (degrades to baseline-only judging).
818
859
  ORG_RULES=$(printf '%s' "$PROPOSED" | head -c 8000 \\
819
860
  | jq -Rs '{content: .}' \\
820
861
  | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
@@ -867,6 +908,7 @@ if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; t
867
908
  --arg cwd "$CWD" \\
868
909
  --arg permission_mode "$PERMISSION_MODE" \\
869
910
  --arg headless_flag "$HEADLESS_FLAG" \\
911
+ --arg repo "$GIT_REPO" \\
870
912
  '{
871
913
  verdict: $verdict,
872
914
  file_path: $file_path,
@@ -880,7 +922,8 @@ if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; t
880
922
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
881
923
  cwd: (if ($cwd | length) > 0 then $cwd else null end),
882
924
  permission_mode: (if ($permission_mode | length) > 0 then $permission_mode else null end),
883
- headless: ($headless_flag == "1")
925
+ headless: ($headless_flag == "1"),
926
+ repo: (if ($repo | length) > 0 then $repo else null end)
884
927
  }')
885
928
 
886
929
  RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit/local-verdict" \\
@@ -917,21 +960,29 @@ else
917
960
  fi
918
961
  fi
919
962
 
920
- # Server returns the literal CC hook JSON shape ({} for allow, or
921
- # hookSpecificOutput.permissionDecision: "deny" + reason). If the call failed
922
- # entirely or returned non-JSON, fail open with empty {}.
923
963
  if [ -z "$RESP" ]; then
924
- echo '{}'
964
+ synkro_log "editGuard $FILE_SHORT \u2192 error (timeout)"
965
+ jq -n --arg m "[synkro] editGuard $FILE_SHORT \u2192 error (timeout)" '{systemMessage: $m}'
925
966
  exit 0
926
967
  fi
927
968
 
928
- # Cheap validation \u2014 only forward if it parses as a JSON object.
929
969
  if ! echo "$RESP" | jq -e 'type == "object"' >/dev/null 2>&1; then
930
- echo '{}'
970
+ synkro_log "editGuard $FILE_SHORT \u2192 error (bad response)"
971
+ jq -n --arg m "[synkro] editGuard $FILE_SHORT \u2192 error (bad response)" '{systemMessage: $m}'
931
972
  exit 0
932
973
  fi
933
974
 
934
- echo "$RESP"
975
+ DECISION=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecision // "allow"' 2>/dev/null)
976
+ if [ "$DECISION" = "deny" ]; then
977
+ DENY_REASON=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecisionReason // ""' 2>/dev/null)
978
+ synkro_log "editGuard $FILE_SHORT \u2192 BLOCKED: $DENY_REASON"
979
+ echo "$RESP"
980
+ else
981
+ synkro_log "editGuard $FILE_SHORT \u2192 pass"
982
+ RESP_WITH_MSG=$(echo "$RESP" | jq --arg m "[synkro] editGuard $FILE_SHORT \u2192 pass" '. + {systemMessage: $m}')
983
+ echo "$RESP_WITH_MSG"
984
+ fi
985
+
935
986
  exit 0
936
987
  `;
937
988
  CC_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
@@ -950,6 +1001,8 @@ exit 0
950
1001
  # Always exits 0 with valid JSON \u2014 never breaks CC's flow.
951
1002
  set -e
952
1003
 
1004
+ synkro_log() { echo "[synkro] $1" >&2; }
1005
+
953
1006
  CONFIG_FILE="$HOME/.synkro/config.env"
954
1007
  if [ -f "$CONFIG_FILE" ]; then
955
1008
  set -a
@@ -987,6 +1040,14 @@ TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
987
1040
  SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
988
1041
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
989
1042
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
1043
+ # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
1044
+ GIT_REPO=""
1045
+ if command -v git >/dev/null 2>&1; then
1046
+ _REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
1047
+ if [ -n "$_REMOTE" ]; then
1048
+ GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
1049
+ fi
1050
+ fi
990
1051
 
991
1052
  FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .notebook_path // .path // empty' 2>/dev/null)
992
1053
  if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
@@ -995,6 +1056,7 @@ if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
995
1056
  fi
996
1057
 
997
1058
  BASENAME=$(basename "$FILE_PATH")
1059
+ synkro_log "editScan checking: $BASENAME"
998
1060
 
999
1061
  # Read post-edit file content (cap 64KB).
1000
1062
  FILE_CONTENT=$(head -c 65536 "$FILE_PATH" 2>/dev/null || echo "")
@@ -1015,13 +1077,15 @@ BODY=$(jq -n \\
1015
1077
  --arg session_id "$SESSION_ID" \\
1016
1078
  --arg tool_use_id "$TOOL_USE_ID" \\
1017
1079
  --arg cwd "$CWD" \\
1080
+ --arg repo "$GIT_REPO" \\
1018
1081
  '{
1019
1082
  file_path: $file_path,
1020
1083
  content: $content,
1021
1084
  diff: $diff,
1022
1085
  session_id: (if ($session_id | length) > 0 then $session_id else null end),
1023
1086
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
1024
- cwd: (if ($cwd | length) > 0 then $cwd else null end)
1087
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
1088
+ repo: (if ($repo | length) > 0 then $repo else null end)
1025
1089
  }')
1026
1090
 
1027
1091
  refresh_jwt() {
@@ -1081,9 +1145,22 @@ SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-free}"
1081
1145
 
1082
1146
  if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
1083
1147
  # \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (mode=edit). \u2500\u2500\u2500
1148
+
1149
+ # Fetch org guardrail rules relevant to this file content.
1150
+ ORG_RULES=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1151
+ | jq -Rs '{content: .}' \\
1152
+ | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
1153
+ -X POST -H "Content-Type: application/json" \\
1154
+ -H "Authorization: Bearer $JWT" \\
1155
+ -d @- --max-time 2 2>/dev/null \\
1156
+ | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1157
+ if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1158
+
1084
1159
  GRADER_PROMPT_FILE=$(mktemp -t synkro-edit-capture.XXXXXX)
1085
1160
  trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
1086
- printf 'File: %s\\n\\nContent:\\n' "$FILE_PATH" > "$GRADER_PROMPT_FILE"
1161
+ printf 'File: %s\\n' "$FILE_PATH" > "$GRADER_PROMPT_FILE"
1162
+ printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
1163
+ printf 'Content:\\n' >> "$GRADER_PROMPT_FILE"
1087
1164
  printf '%s\\n' "$FILE_CONTENT" >> "$GRADER_PROMPT_FILE"
1088
1165
 
1089
1166
  if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && [ -f "$HOME/.synkro/grader-primer-edit.txt" ] && command -v python3 >/dev/null 2>&1; then
@@ -1117,12 +1194,8 @@ else
1117
1194
  fi
1118
1195
 
1119
1196
  if [ -z "$RESP" ] || ! echo "$RESP" | jq -e 'type == "object"' >/dev/null 2>&1; then
1120
- if [ "\${SYNKRO_VERBOSE:-0}" = "1" ]; then
1121
- SYS_MSG="[synkro] afterFileEdit \u2192 scanned \${BASENAME} (grader unavailable)"
1122
- jq -n --arg sys_msg "$SYS_MSG" '{ systemMessage: $sys_msg }'
1123
- exit 0
1124
- fi
1125
- echo '{}'
1197
+ synkro_log "editScan $BASENAME \u2192 error (no response)"
1198
+ jq -n --arg m "[synkro] editScan $BASENAME \u2192 error (no response)" '{systemMessage: $m}'
1126
1199
  exit 0
1127
1200
  fi
1128
1201
 
@@ -1132,7 +1205,8 @@ CATEGORY=$(echo "$RESP" | jq -r '.category // "unspecified"' 2>/dev/null)
1132
1205
  REASON=$(echo "$RESP" | jq -r '.reason // ""' 2>/dev/null)
1133
1206
 
1134
1207
  if [ "$OK" = "false" ] && [ -n "$REASON" ]; then
1135
- SYS_MSG="[synkro] afterFileEdit \u2192 Finding in \${BASENAME}: \${REASON}"
1208
+ synkro_log "editScan $BASENAME \u2192 FAIL ($CATEGORY): $REASON"
1209
+ SYS_MSG="[synkro] editScan $BASENAME \u2192 FAIL: \${REASON}"
1136
1210
  ADDITIONAL_CTX="Synkro post-edit grader flagged \${BASENAME} (severity: \${SEVERITY}, category: \${CATEGORY}). Re-edit the file applying the retry guidance: \${REASON}"
1137
1211
  jq -n \\
1138
1212
  --arg sys_msg "$SYS_MSG" \\
@@ -1147,13 +1221,8 @@ if [ "$OK" = "false" ] && [ -n "$REASON" ]; then
1147
1221
  exit 0
1148
1222
  fi
1149
1223
 
1150
- if [ "\${SYNKRO_VERBOSE:-0}" = "1" ]; then
1151
- SYS_MSG="[synkro] afterFileEdit \u2192 no issues in \${BASENAME}"
1152
- jq -n --arg sys_msg "$SYS_MSG" '{ systemMessage: $sys_msg }'
1153
- exit 0
1154
- fi
1155
-
1156
- echo '{}'
1224
+ synkro_log "editScan $BASENAME \u2192 pass"
1225
+ jq -n --arg m "[synkro] editScan $BASENAME \u2192 pass" '{systemMessage: $m}'
1157
1226
  exit 0
1158
1227
  `;
1159
1228
  CC_STOP_SUMMARY_SCRIPT = `#!/bin/bash
@@ -2143,7 +2212,7 @@ function writeConfigEnv(opts) {
2143
2212
  `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
2144
2213
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
2145
2214
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
2146
- `SYNKRO_VERSION=${shellQuoteSingle("1.1.8")}`
2215
+ `SYNKRO_VERSION=${shellQuoteSingle("1.2.1")}`
2147
2216
  ];
2148
2217
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
2149
2218
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -2473,6 +2542,15 @@ function statusCommand() {
2473
2542
  console.log(` gateway: ${config.SYNKRO_GATEWAY_URL ?? "(unset)"}`);
2474
2543
  console.log(` credentials: ${config.SYNKRO_CREDENTIALS_PATH ?? "(unset)"}`);
2475
2544
  console.log(` tier: ${config.SYNKRO_TIER ?? "(unset)"}`);
2545
+ const info2 = getUserInfo();
2546
+ const userId = info2?.id ?? config.SYNKRO_USER_ID ?? "default";
2547
+ const tierCacheFile = join5(SYNKRO_DIR2, `.tier-cache-${userId}`);
2548
+ let inferenceTier = config.SYNKRO_INFERENCE_TIER || null;
2549
+ if (!inferenceTier && existsSync6(tierCacheFile)) {
2550
+ inferenceTier = readFileSync5(tierCacheFile, "utf-8").trim() || null;
2551
+ }
2552
+ const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
2553
+ console.log(` inference: ${tierLabel}`);
2476
2554
  console.log(` version: ${config.SYNKRO_VERSION ?? "(unset)"}`);
2477
2555
  console.log();
2478
2556
  const agents = detectAgents();