@synkro-sh/cli 1.4.42 → 1.4.45

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
@@ -121,11 +121,13 @@ function installCCHooks(settingsPath, config) {
121
121
  removeSynkroEntries(settings.hooks, "PostToolUse");
122
122
  removeSynkroEntries(settings.hooks, "SessionEnd");
123
123
  removeSynkroEntries(settings.hooks, "SessionStart");
124
+ removeSynkroEntries(settings.hooks, "UserPromptSubmit");
124
125
  removeSynkroEntries(settings.hooks, "Stop");
125
126
  settings.hooks.PreToolUse = settings.hooks.PreToolUse ?? [];
126
127
  settings.hooks.PostToolUse = settings.hooks.PostToolUse ?? [];
127
128
  settings.hooks.SessionEnd = settings.hooks.SessionEnd ?? [];
128
129
  settings.hooks.SessionStart = settings.hooks.SessionStart ?? [];
130
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit ?? [];
129
131
  settings.hooks.PreToolUse.push({
130
132
  matcher: "Bash|Read|Grep|Glob",
131
133
  hooks: [
@@ -181,6 +183,17 @@ function installCCHooks(settingsPath, config) {
181
183
  ],
182
184
  [SYNKRO_MARKER]: true
183
185
  });
186
+ settings.hooks.PostToolUse.push({
187
+ matcher: "Edit|Write|MultiEdit|NotebookEdit",
188
+ hooks: [
189
+ {
190
+ type: "command",
191
+ command: config.cweScanScriptPath,
192
+ timeout: 15
193
+ }
194
+ ],
195
+ [SYNKRO_MARKER]: true
196
+ });
184
197
  settings.hooks.PostToolUse.push({
185
198
  matcher: "Bash",
186
199
  hooks: [
@@ -209,6 +222,16 @@ function installCCHooks(settingsPath, config) {
209
222
  ],
210
223
  [SYNKRO_MARKER]: true
211
224
  });
225
+ settings.hooks.UserPromptSubmit.push({
226
+ hooks: [
227
+ {
228
+ type: "command",
229
+ command: config.userPromptSubmitScriptPath,
230
+ timeout: 5
231
+ }
232
+ ],
233
+ [SYNKRO_MARKER]: true
234
+ });
212
235
  settings.hooks.Stop = settings.hooks.Stop ?? [];
213
236
  removeSynkroEntries(settings.hooks, "Stop");
214
237
  settings.hooks.Stop.push({
@@ -452,7 +475,7 @@ var init_mcpConfig = __esm({
452
475
  });
453
476
 
454
477
  // cli/installer/hookScripts.ts
455
- var SYNKRO_COMMON_SCRIPT, CC_BASH_JUDGE_SCRIPT, CC_EDIT_PRECHECK_SCRIPT, CC_EDIT_CAPTURE_SCRIPT, CC_PLAN_JUDGE_SCRIPT, CC_STOP_SUMMARY_SCRIPT, CC_SESSION_START_SCRIPT, CC_BASH_FOLLOWUP_SCRIPT, CC_CVE_SCAN_SCRIPT, CC_TRANSCRIPT_SYNC_SCRIPT, CURSOR_BASH_JUDGE_SCRIPT, CURSOR_EDIT_PRECHECK_SCRIPT, CURSOR_EDIT_CAPTURE_SCRIPT, CURSOR_BASH_FOLLOWUP_SCRIPT;
478
+ var SYNKRO_COMMON_SCRIPT, CC_BASH_JUDGE_SCRIPT, CC_EDIT_PRECHECK_SCRIPT, CC_EDIT_CAPTURE_SCRIPT, CC_PLAN_JUDGE_SCRIPT, CC_STOP_SUMMARY_SCRIPT, CC_SESSION_START_SCRIPT, CC_BASH_FOLLOWUP_SCRIPT, CC_CVE_SCAN_SCRIPT, CC_CWE_SCAN_SCRIPT, CC_TRANSCRIPT_SYNC_SCRIPT, CURSOR_BASH_JUDGE_SCRIPT, CURSOR_EDIT_PRECHECK_SCRIPT, CURSOR_EDIT_CAPTURE_SCRIPT, CURSOR_BASH_FOLLOWUP_SCRIPT, CC_USER_PROMPT_SUBMIT_SCRIPT;
456
479
  var init_hookScripts = __esm({
457
480
  "cli/installer/hookScripts.ts"() {
458
481
  "use strict";
@@ -519,6 +542,10 @@ synkro_channel_up() {
519
542
  (exec 3<>/dev/tcp/127.0.0.1/\${SYNKRO_CHANNEL_PORT:-8929}) 2>/dev/null && exec 3<&- 3>&-
520
543
  }
521
544
 
545
+ synkro_cwe_channel_up() {
546
+ (exec 3<>/dev/tcp/127.0.0.1/8930) 2>/dev/null && exec 3<&- 3>&-
547
+ }
548
+
522
549
  # Fetch hook config. Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES, SYNKRO_SILENT, SYNKRO_POLICY_NAME.
523
550
  synkro_load_config() {
524
551
  local resp
@@ -547,6 +574,13 @@ synkro_route() {
547
574
  echo "cloud"
548
575
  }
549
576
 
577
+ # Routing for CWE channel (port 8930).
578
+ synkro_cwe_route() {
579
+ [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && echo "local" && return
580
+ synkro_cwe_channel_up && echo "local" && return
581
+ echo "cloud"
582
+ }
583
+
550
584
  # Grade locally via synkro CLI channel. Reads prompt from stdin.
551
585
  synkro_local_grade() {
552
586
  local surface="$1"
@@ -564,11 +598,37 @@ synkro_local_grade() {
564
598
  fi
565
599
  }
566
600
 
601
+ # Grade CWE locally via channel 2 (port 8930). Reads prompt from stdin.
602
+ synkro_local_grade_cwe() {
603
+ if ! synkro_cwe_channel_up; then
604
+ echo "SYNKRO_CHANNEL_DOWN" >&2
605
+ return 1
606
+ fi
607
+ if [ -n "\${SYNKRO_CLI_BIN:-}" ] && [ -f "$SYNKRO_CLI_BIN" ] && command -v node >/dev/null 2>&1; then
608
+ SYNKRO_CHANNEL_PORT=8930 node "$SYNKRO_CLI_BIN" grade cwe 2>/dev/null
609
+ elif command -v synkro >/dev/null 2>&1; then
610
+ SYNKRO_CHANNEL_PORT=8930 synkro grade cwe 2>/dev/null
611
+ else
612
+ echo "SYNKRO_CLI_NOT_FOUND" >&2
613
+ return 1
614
+ fi
615
+ }
616
+
617
+ # Extract the CC model from the current turn's transcript. Call after reading PAYLOAD.
618
+ # Sets CC_MODEL to the model used in the most recent assistant message.
619
+ synkro_detect_cc_model() {
620
+ CC_MODEL=""
621
+ local tp
622
+ tp=$(echo "\${1:-$PAYLOAD}" | jq -r '.transcript_path // empty' 2>/dev/null)
623
+ [ -z "$tp" ] || [ ! -f "$tp" ] && return
624
+ CC_MODEL=$(grep '"type":"assistant"' "$tp" 2>/dev/null | tail -1 | jq -r '.message.model // empty' 2>/dev/null)
625
+ }
626
+
567
627
  # Parse <synkro-verdict>...</synkro-verdict> XML from local grader output.
568
628
  # Sets LOCAL_OK, LOCAL_REASON, LOCAL_RULE_ID, LOCAL_RULE_MODE, LOCAL_SEV, LOCAL_CAT.
569
629
  synkro_parse_local_verdict() {
570
630
  local resp="$1"
571
- LOCAL_OK="true"; LOCAL_REASON=""; LOCAL_RULE_ID=""; LOCAL_RULE_MODE=""; LOCAL_SEV="low"; LOCAL_CAT="general"
631
+ LOCAL_OK="true"; LOCAL_REASON=""; LOCAL_RULE_ID=""; LOCAL_RULE_MODE=""; LOCAL_SEV="low"; LOCAL_CAT="clean"
572
632
  local inner
573
633
  inner=$(printf '%s' "$resp" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
574
634
  [ -z "$inner" ] && return
@@ -581,16 +641,16 @@ synkro_parse_local_verdict() {
581
641
  LOCAL_RULE_ID=$(printf '%s' "$inner" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
582
642
  LOCAL_RULE_MODE=$(printf '%s' "$inner" | sed -nE 's|.*<rule_mode>(.*)</rule_mode>.*|\\1|p' | head -1)
583
643
  LOCAL_SEV=$(printf '%s' "$inner" | sed -nE 's|.*<risk_level>(.*)</risk_level>.*|\\1|p' | head -1)
584
- LOCAL_CAT=$(printf '%s' "$inner" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
585
644
  if [ -z "$LOCAL_RULE_ID" ]; then
586
645
  local fv
587
646
  fv=$(printf '%s' "$inner" | awk -v RS='</violation>' '/<violation>/{print; exit}')
588
647
  LOCAL_RULE_ID=$(printf '%s' "$fv" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
589
648
  [ -z "$LOCAL_REASON" ] && LOCAL_REASON=$(printf '%s' "$fv" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
590
649
  [ -z "$LOCAL_SEV" ] && LOCAL_SEV=$(printf '%s' "$fv" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
591
- [ -z "$LOCAL_CAT" ] && LOCAL_CAT=$(printf '%s' "$fv" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
592
650
  fi
593
- LOCAL_SEV="\${LOCAL_SEV:-high}"; LOCAL_CAT="\${LOCAL_CAT:-policy_violation}"
651
+ LOCAL_SEV="\${LOCAL_SEV:-high}"
652
+ LOCAL_CAT=$(printf '%s' "$inner" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
653
+ LOCAL_CAT="\${LOCAL_CAT:-uncategorized}"
594
654
  [ -z "$LOCAL_RULE_ID" ] && LOCAL_RULE_ID=$(printf '%s' "$LOCAL_REASON" | grep -oE '[Rr][0-9]{3}' | head -1)
595
655
  fi
596
656
  }
@@ -598,12 +658,13 @@ synkro_parse_local_verdict() {
598
658
  # Fire anonymized telemetry for local verdicts. All args positional.
599
659
  synkro_capture_local() {
600
660
  local hook_type="$1" verdict="$2" severity="$3" category="$4" tool_name="$5" repo="$6" session_id="$7"
661
+ local cc_model="\${CC_MODEL:-unknown}"
601
662
  (
602
663
  BODY=$(jq -n \\
603
664
  --arg eid "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
604
665
  --arg ht "$hook_type" --arg v "$verdict" --arg s "$severity" --arg c "$category" \\
605
- --arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" \\
606
- '{capture_type:"local_verdict",event_id:$eid,hook_type:$ht,verdict:$v,severity:$s,category:$c,model:"claude-sonnet-4-6",tool_name:$tn}
666
+ --arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" --arg mdl "$cc_model" \\
667
+ '{capture_type:"local_verdict",event_id:$eid,hook_type:$ht,verdict:$v,severity:$s,category:$c,cc_model:$mdl,model:$mdl,tool_name:$tn}
607
668
  + (if $r != "" then {repo:$r} else {} end)
608
669
  + (if $sid != "" then {session_id:$sid} else {} end)')
609
670
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/hook/capture" \\
@@ -616,15 +677,16 @@ synkro_capture_local() {
616
677
  synkro_capture_local_full() {
617
678
  local hook_type="$1" verdict="$2" severity="$3" category="$4" tool_name="$5" repo="$6" session_id="$7"
618
679
  local command="$8" reasoning="$9" rules_checked="\${10:-[]}" violated_rules="\${11:-[]}" recent_user_messages="\${12:-[]}"
680
+ local cc_model="\${CC_MODEL:-unknown}"
619
681
  (
620
682
  BODY=$(jq -n \\
621
683
  --arg eid "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
622
684
  --arg ht "$hook_type" --arg v "$verdict" --arg s "$severity" --arg c "$category" \\
623
- --arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" \\
685
+ --arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" --arg mdl "$cc_model" \\
624
686
  --arg cmd "$command" --arg rsn "$reasoning" --arg cd "$SYNKRO_CAPTURE_DEPTH" \\
625
687
  --argjson rc "$rules_checked" --argjson vr "$violated_rules" --argjson rum "$recent_user_messages" \\
626
688
  '{capture_type:"local_verdict",event_id:$eid,hook_type:$ht,verdict:$v,severity:$s,category:$c,
627
- model:"claude-sonnet-4-6",tool_name:$tn,capture_depth:$cd,
689
+ cc_model:$mdl,model:$mdl,tool_name:$tn,capture_depth:$cd,
628
690
  command:(if ($cmd|length) > 0 then $cmd else null end),
629
691
  reasoning:(if ($rsn|length) > 0 then $rsn else null end),
630
692
  rules_checked:$rc, violated_rules:$vr, recent_user_messages:$rum}
@@ -700,6 +762,13 @@ synkro_consent_clear_consumed() {
700
762
  grep -v "^\${sid}\${_TAB}\${hash}\${_TAB}consumed$" "$SYNKRO_CONSENT_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$SYNKRO_CONSENT_FILE" 2>/dev/null || true
701
763
  }
702
764
 
765
+ SYNKRO_LAST_PROMPT_FILE="$HOME/.synkro/.last-prompt"
766
+
767
+ synkro_read_last_prompt() {
768
+ [ -f "$SYNKRO_LAST_PROMPT_FILE" ] || return
769
+ cat "$SYNKRO_LAST_PROMPT_FILE" 2>/dev/null
770
+ }
771
+
703
772
  synkro_post_with_retry() {
704
773
  local url="$1" body="$2" timeout="\${3:-8}"
705
774
  local resp
@@ -737,6 +806,7 @@ TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
737
806
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
738
807
  GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
739
808
  PERMISSION_MODE=$(echo "$PAYLOAD" | jq -r '.permission_mode // empty' 2>/dev/null)
809
+ synkro_detect_cc_model
740
810
 
741
811
  # Translate tool calls to command string for logging
742
812
  case "$TOOL_NAME" in
@@ -762,6 +832,8 @@ fi
762
832
  IS_HEADLESS="\${SYNKRO_HEADLESS:-0}"
763
833
  case "$PERMISSION_MODE" in acceptEdits|bypassPermissions|plan|auto) IS_HEADLESS="1" ;; esac
764
834
 
835
+ LAST_PROMPT=$(synkro_read_last_prompt)
836
+
765
837
  synkro_load_config
766
838
  ROUTE=$(synkro_route)
767
839
  TAG=$(synkro_tag "$ROUTE")
@@ -775,7 +847,7 @@ if [ "$ROUTE" = "local" ]; then
775
847
  # \u2500\u2500\u2500 Local grading (local_only privacy or local-cc channel) \u2500\u2500\u2500
776
848
  GRADER_FILE=$(mktemp -t synkro-bash.XXXXXX)
777
849
  trap "rm -f \\"$GRADER_FILE\\"" EXIT
778
- printf 'Working directory: %s\\nRepo: %s\\nCommand: %s\\nUser intent (last human message): %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$COMMAND" "\${USER_INTENT:-none stated}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
850
+ printf 'Working directory: %s\\nRepo: %s\\nCommand: %s\\nUser intent (last human message): %s\\nLast user prompt: %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$COMMAND" "\${USER_INTENT:-none stated}" "\${LAST_PROMPT:-none}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
779
851
 
780
852
  CC_RESP=$(synkro_local_grade bash < "$GRADER_FILE" 2>&1)
781
853
  if [ $? -ne 0 ]; then
@@ -811,7 +883,6 @@ if [ "$ROUTE" = "local" ]; then
811
883
  fi
812
884
 
813
885
  # \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
814
- CC_MODEL=""
815
886
  CC_USAGE="{}"
816
887
  RECENT_MESSAGES="[]"
817
888
  RECENT_ACTIONS="[]"
@@ -819,7 +890,6 @@ SESSION_SUMMARY=""
819
890
  if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
820
891
  _LAST=$(grep '"type":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
821
892
  if [ -n "$_LAST" ]; then
822
- CC_MODEL=$(echo "$_LAST" | jq -r '.message.model // empty' 2>/dev/null)
823
893
  CC_USAGE=$(echo "$_LAST" | jq -c '{input_tokens:.message.usage.input_tokens,output_tokens:.message.usage.output_tokens,cache_creation_input_tokens:.message.usage.cache_creation_input_tokens,cache_read_input_tokens:.message.usage.cache_read_input_tokens}' 2>/dev/null || echo "{}")
824
894
  fi
825
895
  RECENT_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "user" or .type == "assistant") | {type, text: (.message.content | if type == "string" then .[0:500] else ([.[]? | (.text? // "") | .[0:300]] | join(" ")) end)}] | .[-10:]' 2>/dev/null || echo "[]")
@@ -843,11 +913,13 @@ BODY=$(jq -n \\
843
913
  --arg cc_model "$CC_MODEL" \\
844
914
  --argjson cc_usage "$CC_USAGE" \\
845
915
  --arg session_summary "$SESSION_SUMMARY" \\
916
+ --arg last_prompt "\${LAST_PROMPT:-}" \\
846
917
  '{
847
918
  hook_event: $hook_event,
848
919
  tool_name: $tool_name,
849
920
  tool_input: $tool_input,
850
921
  user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
922
+ last_user_message: (if ($last_prompt | length) > 0 then $last_prompt else null end),
851
923
  recent_user_messages: $recent_user_messages,
852
924
  recent_messages: $recent_messages,
853
925
  recent_actions: $recent_actions,
@@ -898,6 +970,7 @@ TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
898
970
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
899
971
  GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
900
972
  PERMISSION_MODE=$(echo "$PAYLOAD" | jq -r '.permission_mode // empty' 2>/dev/null)
973
+ synkro_detect_cc_model
901
974
 
902
975
  FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .notebook_path // .path // empty' 2>/dev/null)
903
976
  if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
@@ -950,13 +1023,19 @@ DIFF_FIELD=$(echo "$TOOL_INPUT" | jq -c '{old_string, new_string, edits} | with_
950
1023
 
951
1024
  # Extract user intent from transcript
952
1025
  USER_INTENT=""
1026
+ RECENT_USER_MESSAGES="[]"
1027
+ RECENT_MESSAGES="[]"
953
1028
  RECENT_ACTIONS="[]"
954
1029
  TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
955
1030
  if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
956
- USER_INTENT=$(tail -200 "$TRANSCRIPT_PATH" | jq -r 'select(.type == "user") | .message.content | if type == "string" then . else (map(.text? // "") | join(" ")) end' 2>/dev/null | tail -1 || echo "")
1031
+ RECENT_USER_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "user") | (.message.content | if type == "string" then . else (map(.text? // "") | join(" ")) end) | select(. != null and . != "")] | .[-5:]' 2>/dev/null || echo "[]")
1032
+ USER_INTENT=$(echo "$RECENT_USER_MESSAGES" | jq -r '.[-1] // ""' 2>/dev/null || echo "")
1033
+ RECENT_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "user" or .type == "assistant") | {type, text: (.message.content | if type == "string" then .[0:500] else ([.[]? | (.text? // "") | .[0:300]] | join(" ")) end)}] | .[-10:]' 2>/dev/null || echo "[]")
957
1034
  RECENT_ACTIONS=$(tail -200 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | {tool: .name, input: (.input // {} | tostring | .[0:200])}] | .[-5:]' 2>/dev/null || echo "[]")
958
1035
  fi
959
1036
 
1037
+ LAST_PROMPT=$(synkro_read_last_prompt)
1038
+
960
1039
  synkro_load_config
961
1040
  ROUTE=$(synkro_route)
962
1041
  TAG=$(synkro_tag "$ROUTE")
@@ -970,7 +1049,7 @@ if [ "$ROUTE" = "local" ]; then
970
1049
  # \u2500\u2500\u2500 Local grading (local_only privacy or local-cc channel) \u2500\u2500\u2500
971
1050
  GRADER_FILE=$(mktemp -t synkro-edit.XXXXXX)
972
1051
  trap "rm -f \\"$GRADER_FILE\\"" EXIT
973
- printf 'Working directory: %s\\nRepo: %s\\nFile: %s\\nProposed content (first 4000 chars):\\n%s\\nUser intent (last human message): %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$FILE_PATH" "$(printf '%s' "$PROPOSED" | head -c 4000)" "\${USER_INTENT:-none stated}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
1052
+ printf 'Working directory: %s\\nRepo: %s\\nFile: %s\\nProposed content (first 4000 chars):\\n%s\\nUser intent (last human message): %s\\nLast user prompt: %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$FILE_PATH" "$(printf '%s' "$PROPOSED" | head -c 4000)" "\${USER_INTENT:-none stated}" "\${LAST_PROMPT:-none}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
974
1053
 
975
1054
  CC_RESP=$(synkro_local_grade edit < "$GRADER_FILE" 2>&1)
976
1055
  if [ $? -ne 0 ]; then
@@ -1016,6 +1095,8 @@ BODY=$(jq -n \\
1016
1095
  --arg file_before "$FILE_BEFORE" \\
1017
1096
  --argjson diff "$DIFF_FIELD" \\
1018
1097
  --arg user_intent "$USER_INTENT" \\
1098
+ --argjson recent_user_messages "$RECENT_USER_MESSAGES" \\
1099
+ --argjson recent_messages "$RECENT_MESSAGES" \\
1019
1100
  --argjson recent_actions "$RECENT_ACTIONS" \\
1020
1101
  --arg session_id "$SESSION_ID" \\
1021
1102
  --arg tool_use_id "$TOOL_USE_ID" \\
@@ -1023,6 +1104,7 @@ BODY=$(jq -n \\
1023
1104
  --arg repo "$GIT_REPO" \\
1024
1105
  --arg permission_mode "$PERMISSION_MODE" \\
1025
1106
  --arg headless_flag "\${SYNKRO_HEADLESS:-0}" \\
1107
+ --arg last_prompt "\${LAST_PROMPT:-}" \\
1026
1108
  '{
1027
1109
  hook_event: $hook_event,
1028
1110
  tool_name: $tool_name,
@@ -1032,6 +1114,9 @@ BODY=$(jq -n \\
1032
1114
  file_before: (if ($file_before | length) > 0 then $file_before else null end),
1033
1115
  diff: $diff,
1034
1116
  user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
1117
+ last_user_message: (if ($last_prompt | length) > 0 then $last_prompt else null end),
1118
+ recent_user_messages: $recent_user_messages,
1119
+ recent_messages: $recent_messages,
1035
1120
  recent_actions: $recent_actions,
1036
1121
  session_id: (if ($session_id | length) > 0 then $session_id else null end),
1037
1122
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
@@ -1089,6 +1174,7 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
1089
1174
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
1090
1175
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
1091
1176
  GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
1177
+ synkro_detect_cc_model
1092
1178
 
1093
1179
  # Correction followup (backgrounded)
1094
1180
  if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
@@ -1235,6 +1321,7 @@ if [ -z "$PLAN" ] || [ \${#PLAN} -lt 20 ]; then echo '{}'; exit 0; fi
1235
1321
  SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
1236
1322
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
1237
1323
  GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
1324
+ synkro_detect_cc_model
1238
1325
 
1239
1326
  PLAN_SHORT=$(printf '%s' "$PLAN" | head -c 80)
1240
1327
  synkro_log "planReview checking: $PLAN_SHORT..."
@@ -1368,7 +1455,7 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
1368
1455
  BODY=$(jq -n \\
1369
1456
  --arg event_id "usage_$(date +%s)_$$" \\
1370
1457
  --arg hook_type "stop" --arg verdict "allow" --arg severity "none" \\
1371
- --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1458
+ --arg model "\${CC_MODEL:-unknown}" \\
1372
1459
  --arg cc_model "\${CC_MODEL:-}" \\
1373
1460
  --arg repo "\${GIT_REPO:-}" --arg session_id "$SESSION_ID" \\
1374
1461
  --argjson cc_usage "$CC_USAGE" \\
@@ -1581,6 +1668,121 @@ else
1581
1668
  jq -n --arg m "[synkro:\${ROUTE}:cveScan] clean" '{systemMessage: $m}'
1582
1669
  fi
1583
1670
  exit 0
1671
+ `;
1672
+ CC_CWE_SCAN_SCRIPT = `#!/bin/bash
1673
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
1674
+ . "$SCRIPT_DIR/_synkro-common.sh"
1675
+
1676
+ JWT=$(synkro_load_jwt)
1677
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
1678
+ synkro_ensure_fresh_jwt
1679
+
1680
+ PAYLOAD=$(cat)
1681
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
1682
+
1683
+ TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
1684
+ case "$TOOL_NAME" in Edit|Write|MultiEdit|NotebookEdit) ;; *) echo '{}'; exit 0 ;; esac
1685
+
1686
+ TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
1687
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
1688
+
1689
+ FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .notebook_path // .path // empty' 2>/dev/null)
1690
+ if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then echo '{}'; exit 0; fi
1691
+
1692
+ FILE_CONTENT=$(head -c 65536 "$FILE_PATH" 2>/dev/null || echo "")
1693
+ if [ -z "$FILE_CONTENT" ]; then echo '{}'; exit 0; fi
1694
+
1695
+ synkro_load_config
1696
+ ROUTE=$(synkro_route)
1697
+ TAG=$(synkro_tag "$ROUTE")
1698
+
1699
+ if [ "$SYNKRO_SILENT" = "true" ]; then echo '{}'; exit 0; fi
1700
+
1701
+ BASENAME=$(basename "$FILE_PATH")
1702
+ FILE_EXT=".\${FILE_PATH##*.}"
1703
+
1704
+ # \u2500\u2500\u2500 Helper: format CWE findings into system message \u2500\u2500\u2500
1705
+ format_cwe_result() {
1706
+ local count="$1" crit="$2" ids="$3" total="$4"
1707
+ [ "$count" = "1" ] && local label="match" || local label="matches"
1708
+ if [ "$crit" -gt 0 ] 2>/dev/null; then
1709
+ [ "$total" -gt 3 ] && ids="\${ids}, ..."
1710
+ jq -n --arg m "[synkro:\${ROUTE}:cweScan] \${count} CWE \${label}, \${crit} critical (\${ids})" '{systemMessage: $m}'
1711
+ else
1712
+ [ "$total" -gt 3 ] && ids="\${ids}, ..."
1713
+ jq -n --arg m "[synkro:\${ROUTE}:cweScan] \${count} CWE \${label} (\${ids})" '{systemMessage: $m}'
1714
+ fi
1715
+ }
1716
+
1717
+ if [ "$ROUTE" = "local" ]; then
1718
+ # \u2500\u2500\u2500 Local CWE scan: deterministic Top 25 filter + local-cc grader \u2500\u2500\u2500
1719
+ CWE_RULES=$(curl -sS -X GET "\${GATEWAY_URL}/api/v1/cwe-rules?ext=$FILE_EXT" \\
1720
+ -H "Authorization: Bearer $JWT" --max-time 4 2>/dev/null || echo "")
1721
+ CWE_LIST=$(echo "$CWE_RULES" | jq -c '.rules // []' 2>/dev/null || echo "[]")
1722
+ CWE_RULE_COUNT=$(echo "$CWE_LIST" | jq 'length' 2>/dev/null || echo "0")
1723
+ if [ "$CWE_RULE_COUNT" -eq 0 ] 2>/dev/null; then
1724
+ jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean (no CWEs for $FILE_EXT)" '{systemMessage: $m}'
1725
+ exit 0
1726
+ fi
1727
+
1728
+ GRADER_FILE=$(mktemp -t synkro-cwescan.XXXXXX)
1729
+ trap "rm -f \\"$GRADER_FILE\\"" EXIT
1730
+ printf 'File: %s\\nContent (first 4000 chars):\\n%s\\n\\nCWE rules to check against:\\n%s\\n' "$FILE_PATH" "$(printf '%s' "$FILE_CONTENT" | head -c 4000)" "$CWE_LIST" > "$GRADER_FILE"
1731
+
1732
+ CC_RESP=$(synkro_local_grade_cwe < "$GRADER_FILE" 2>&1)
1733
+ if [ $? -ne 0 ]; then
1734
+ jq -n --arg m "[synkro:\${ROUTE}:cweScan] pass: local grader unavailable" '{systemMessage: $m}'
1735
+ exit 0
1736
+ fi
1737
+ synkro_parse_local_verdict "$CC_RESP"
1738
+
1739
+ if [ "$LOCAL_OK" = "false" ]; then
1740
+ CWE_IDS=""
1741
+ CWE_COUNT=0
1742
+ CWE_CRIT=0
1743
+ while IFS= read -r vid; do
1744
+ [ -z "$vid" ] && continue
1745
+ CWE_COUNT=$((CWE_COUNT + 1))
1746
+ cwe_tag=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | grep -oE "<violation>[^<]*<rule_id>$vid</rule_id>[^<]*<severity>[^<]*</severity>" | head -1)
1747
+ sev=$(printf '%s' "$cwe_tag" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p')
1748
+ [ "$sev" = "critical" ] && CWE_CRIT=$((CWE_CRIT + 1))
1749
+ CWE_ID=$(echo "$vid" | sed 's/cwe-/CWE-/')
1750
+ [ "$CWE_COUNT" -le 3 ] && { [ -n "$CWE_IDS" ] && CWE_IDS="$CWE_IDS, $CWE_ID" || CWE_IDS="$CWE_ID"; }
1751
+ done <<< "$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | grep -oE '<rule_id>[^<]+</rule_id>' | sed 's/<[^>]*>//g')"
1752
+
1753
+ if [ "$CWE_COUNT" -gt 0 ]; then
1754
+ format_cwe_result "$CWE_COUNT" "$CWE_CRIT" "$CWE_IDS" "$CWE_COUNT"
1755
+ else
1756
+ jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean" '{systemMessage: $m}'
1757
+ fi
1758
+ else
1759
+ jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean" '{systemMessage: $m}'
1760
+ fi
1761
+ exit 0
1762
+ fi
1763
+
1764
+ # \u2500\u2500\u2500 Cloud CWE scan: cosine similarity against full CWE catalog \u2500\u2500\u2500
1765
+ BODY=$(jq -n --arg fp "$FILE_PATH" --arg c "$FILE_CONTENT" \\
1766
+ '{file_path:$fp, content:$c}')
1767
+
1768
+ RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/cwe-scan" \\
1769
+ -H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \\
1770
+ -d "$BODY" --max-time 8 2>/dev/null || echo "")
1771
+
1772
+ if [ -z "$RESP" ] || ! echo "$RESP" | jq -e 'type == "object"' >/dev/null 2>&1; then
1773
+ echo '{}'; exit 0
1774
+ fi
1775
+
1776
+ CWE_COUNT=$(echo "$RESP" | jq -r '.findings | length' 2>/dev/null || echo "0")
1777
+ if [ "$CWE_COUNT" -gt 0 ] 2>/dev/null; then
1778
+ CWE_CRIT=$(echo "$RESP" | jq '[.findings[] | select(.severity == "critical" or .mode == "blocking")] | length' 2>/dev/null || echo "0")
1779
+ CWE_IDS=$(echo "$RESP" | jq -r '[.findings[:3][] | .cwe] | join(", ")' 2>/dev/null || echo "")
1780
+ CWE_TOTAL=$(echo "$RESP" | jq -r '.findings | length' 2>/dev/null || echo "0")
1781
+ format_cwe_result "$CWE_COUNT" "$CWE_CRIT" "$CWE_IDS" "$CWE_TOTAL"
1782
+ else
1783
+ jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean" '{systemMessage: $m}'
1784
+ fi
1785
+ exit 0
1584
1786
  `;
1585
1787
  CC_TRANSCRIPT_SYNC_SCRIPT = `#!/bin/bash
1586
1788
  SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
@@ -1612,7 +1814,7 @@ if [ -n "$_LAST_ASST" ]; then
1612
1814
  _BODY=$(jq -n \\
1613
1815
  --arg event_id "usage_$(date +%s)_$$" \\
1614
1816
  --arg hook_type "stop" --arg verdict "allow" --arg severity "none" \\
1615
- --arg model "\${_CC_MODEL:-claude-sonnet-4-6}" \\
1817
+ --arg model "\${_CC_MODEL:-unknown}" \\
1616
1818
  --arg cc_model "\${_CC_MODEL:-}" \\
1617
1819
  --arg session_id "$SESSION_ID" \\
1618
1820
  --argjson cc_usage "$_USAGE" \\
@@ -1895,6 +2097,19 @@ fi
1895
2097
 
1896
2098
  echo '{}'
1897
2099
  exit 0
2100
+ `;
2101
+ CC_USER_PROMPT_SUBMIT_SCRIPT = `#!/bin/bash
2102
+ # Synkro UserPromptSubmit hook \u2014 stashes the user's last message so PreToolUse
2103
+ # hooks can pass it to the grader for consent detection. No regex matching here;
2104
+ # the grader (local or cloud) decides whether the message constitutes consent.
2105
+ PROMPT_FILE="$HOME/.synkro/.last-prompt"
2106
+ PAYLOAD=$(cat)
2107
+ if [ -z "$PAYLOAD" ]; then exit 0; fi
2108
+ MSG=$(echo "$PAYLOAD" | jq -r '.message // .prompt // .content // empty' 2>/dev/null)
2109
+ if [ -n "$MSG" ]; then
2110
+ printf '%s' "$MSG" > "$PROMPT_FILE" 2>/dev/null || true
2111
+ fi
2112
+ exit 0
1898
2113
  `;
1899
2114
  }
1900
2115
  });
@@ -3223,27 +3438,40 @@ function writePluginFiles() {
3223
3438
  PLUGIN_SETTINGS_PATH,
3224
3439
  JSON.stringify({
3225
3440
  fastMode: true,
3226
- // Pre-approve the project-local synkro-local MCP server so claude doesn't
3227
- // block on a consent prompt at startup. Lives in the PROJECT settings so
3228
- // it's still picked up under --setting-sources project,local (which
3229
- // skips user settings to avoid synkro-hook recursion in the grader).
3230
3441
  enabledMcpjsonServers: ["synkro-local"]
3231
3442
  }, null, 2) + "\n",
3232
3443
  "utf-8"
3233
3444
  );
3234
3445
  writeFileSync6(RUN_SCRIPT_PATH, RUN_SCRIPT_SOURCE, "utf-8");
3235
3446
  chmodSync(RUN_SCRIPT_PATH, 493);
3447
+ mkdirSync6(SESSION_DIR_2, { recursive: true });
3448
+ mkdirSync6(PLUGIN_SETTINGS_DIR_2, { recursive: true });
3449
+ writeFileSync6(PLUGIN_PATH_2, CHANNEL_PLUGIN_SOURCE, "utf-8");
3450
+ chmodSync(PLUGIN_PATH_2, 493);
3451
+ writeFileSync6(PLUGIN_PKG_PATH_2, PLUGIN_PACKAGE_JSON, "utf-8");
3452
+ writeFileSync6(
3453
+ PLUGIN_SETTINGS_PATH_2,
3454
+ JSON.stringify({
3455
+ fastMode: true,
3456
+ enabledMcpjsonServers: ["synkro-local"]
3457
+ }, null, 2) + "\n",
3458
+ "utf-8"
3459
+ );
3460
+ writeFileSync6(RUN_SCRIPT_PATH_2, RUN_SCRIPT_SOURCE_2, "utf-8");
3461
+ chmodSync(RUN_SCRIPT_PATH_2, 493);
3236
3462
  }
3237
3463
  function runBunInstall() {
3238
- const r = spawnSync("bun", ["install", "--silent"], {
3239
- cwd: SESSION_DIR,
3240
- encoding: "utf-8",
3241
- timeout: 12e4
3242
- });
3243
- if (r.status !== 0) {
3244
- throw new LocalCCInstallError(
3245
- `bun install failed in ${SESSION_DIR}: ${r.stderr || r.stdout || "unknown"}`
3246
- );
3464
+ for (const dir of [SESSION_DIR, SESSION_DIR_2]) {
3465
+ const r = spawnSync("bun", ["install", "--silent"], {
3466
+ cwd: dir,
3467
+ encoding: "utf-8",
3468
+ timeout: 12e4
3469
+ });
3470
+ if (r.status !== 0) {
3471
+ throw new LocalCCInstallError(
3472
+ `bun install failed in ${dir}: ${r.stderr || r.stdout || "unknown"}`
3473
+ );
3474
+ }
3247
3475
  }
3248
3476
  }
3249
3477
  function safelyMutateClaudeJson(mutator) {
@@ -3315,6 +3543,15 @@ function writeProjectMcpJson() {
3315
3543
  }
3316
3544
  };
3317
3545
  writeFileSync6(PROJECT_MCP_PATH, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
3546
+ const mcp2 = {
3547
+ mcpServers: {
3548
+ [MCP_SERVER_NAME]: {
3549
+ command: "bun",
3550
+ args: [PLUGIN_PATH_2]
3551
+ }
3552
+ }
3553
+ };
3554
+ writeFileSync6(PROJECT_MCP_PATH_2, JSON.stringify(mcp2, null, 2) + "\n", "utf-8");
3318
3555
  }
3319
3556
  function patchClaudeJson() {
3320
3557
  safelyMutateClaudeJson((parsed) => {
@@ -3327,20 +3564,22 @@ function patchClaudeJson() {
3327
3564
  parsed.projects = {};
3328
3565
  }
3329
3566
  const projects = parsed.projects;
3330
- const existing = projects[SESSION_DIR] && typeof projects[SESSION_DIR] === "object" ? projects[SESSION_DIR] : {};
3331
- const wantEnabled = Array.from(/* @__PURE__ */ new Set([
3332
- ...existing.enabledMcpjsonServers ?? [],
3333
- MCP_SERVER_NAME
3334
- ]));
3335
- const next = {
3336
- ...existing,
3337
- hasTrustDialogAccepted: true,
3338
- hasCompletedProjectOnboarding: true,
3339
- enabledMcpjsonServers: wantEnabled
3340
- };
3341
- if (existing.hasTrustDialogAccepted !== true || existing.hasCompletedProjectOnboarding !== true || JSON.stringify(existing.enabledMcpjsonServers ?? []) !== JSON.stringify(wantEnabled)) {
3342
- projects[SESSION_DIR] = next;
3343
- dirty = true;
3567
+ for (const dir of [SESSION_DIR, SESSION_DIR_2]) {
3568
+ const existing = projects[dir] && typeof projects[dir] === "object" ? projects[dir] : {};
3569
+ const wantEnabled = Array.from(/* @__PURE__ */ new Set([
3570
+ ...existing.enabledMcpjsonServers ?? [],
3571
+ MCP_SERVER_NAME
3572
+ ]));
3573
+ const next = {
3574
+ ...existing,
3575
+ hasTrustDialogAccepted: true,
3576
+ hasCompletedProjectOnboarding: true,
3577
+ enabledMcpjsonServers: wantEnabled
3578
+ };
3579
+ if (existing.hasTrustDialogAccepted !== true || existing.hasCompletedProjectOnboarding !== true || JSON.stringify(existing.enabledMcpjsonServers ?? []) !== JSON.stringify(wantEnabled)) {
3580
+ projects[dir] = next;
3581
+ dirty = true;
3582
+ }
3344
3583
  }
3345
3584
  return dirty;
3346
3585
  });
@@ -3375,14 +3614,16 @@ function uninstallLocalCC() {
3375
3614
  delete parsed.mcpServers[MCP_SERVER_NAME];
3376
3615
  dirty = true;
3377
3616
  }
3378
- if (parsed.projects && typeof parsed.projects === "object" && parsed.projects[SESSION_DIR]) {
3379
- delete parsed.projects[SESSION_DIR];
3380
- dirty = true;
3617
+ for (const dir of [SESSION_DIR, SESSION_DIR_2]) {
3618
+ if (parsed.projects && typeof parsed.projects === "object" && parsed.projects[dir]) {
3619
+ delete parsed.projects[dir];
3620
+ dirty = true;
3621
+ }
3381
3622
  }
3382
3623
  return dirty;
3383
3624
  });
3384
3625
  }
3385
- var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_SETTINGS_DIR, PLUGIN_SETTINGS_PATH, PROJECT_MCP_PATH, CLAUDE_JSON_PATH, RUN_SCRIPT_PATH, TMUX_SESSION_NAME, RUN_SCRIPT_SOURCE, MCP_SERVER_NAME, PLUGIN_PACKAGE_JSON, LocalCCInstallError;
3626
+ var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_SETTINGS_DIR, PLUGIN_SETTINGS_PATH, PROJECT_MCP_PATH, CLAUDE_JSON_PATH, RUN_SCRIPT_PATH, TMUX_SESSION_NAME, SESSION_DIR_2, PLUGIN_PATH_2, PLUGIN_PKG_PATH_2, PLUGIN_SETTINGS_DIR_2, PLUGIN_SETTINGS_PATH_2, PROJECT_MCP_PATH_2, RUN_SCRIPT_PATH_2, TMUX_SESSION_NAME_2, CHANNEL_2_PORT, RUN_SCRIPT_SOURCE, RUN_SCRIPT_SOURCE_2, MCP_SERVER_NAME, PLUGIN_PACKAGE_JSON, LocalCCInstallError;
3386
3627
  var init_install = __esm({
3387
3628
  "cli/local-cc/install.ts"() {
3388
3629
  "use strict";
@@ -3397,6 +3638,15 @@ var init_install = __esm({
3397
3638
  CLAUDE_JSON_PATH = join7(homedir6(), ".claude.json");
3398
3639
  RUN_SCRIPT_PATH = join7(SESSION_DIR, "run-claude.sh");
3399
3640
  TMUX_SESSION_NAME = "synkro-local-cc";
3641
+ SESSION_DIR_2 = join7(homedir6(), ".synkro", "cc_sessions_2");
3642
+ PLUGIN_PATH_2 = join7(SESSION_DIR_2, "synkro-channel.ts");
3643
+ PLUGIN_PKG_PATH_2 = join7(SESSION_DIR_2, "package.json");
3644
+ PLUGIN_SETTINGS_DIR_2 = join7(SESSION_DIR_2, ".claude");
3645
+ PLUGIN_SETTINGS_PATH_2 = join7(PLUGIN_SETTINGS_DIR_2, "settings.json");
3646
+ PROJECT_MCP_PATH_2 = join7(SESSION_DIR_2, ".mcp.json");
3647
+ RUN_SCRIPT_PATH_2 = join7(SESSION_DIR_2, "run-claude.sh");
3648
+ TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
3649
+ CHANNEL_2_PORT = 8930;
3400
3650
  RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
3401
3651
  # Auto-generated by \`synkro install\`. Do not edit.
3402
3652
  set -uo pipefail
@@ -3462,6 +3712,62 @@ while tmux has-session -t "$SESSION" 2>/dev/null; do
3462
3712
  sleep 5
3463
3713
  done
3464
3714
 
3715
+ log "tmux session ended."
3716
+ `;
3717
+ RUN_SCRIPT_SOURCE_2 = `#!/usr/bin/env bash
3718
+ # Auto-generated by \`synkro install\`. Channel 2 (CWE scan, port ${CHANNEL_2_PORT}).
3719
+ set -uo pipefail
3720
+
3721
+ SESSION=${TMUX_SESSION_NAME_2}
3722
+ LOG="$HOME/.synkro/cc_sessions_2/run-claude.log"
3723
+
3724
+ log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"; echo "$*"; }
3725
+
3726
+ if ! command -v claude >/dev/null 2>&1; then
3727
+ log "ERROR: claude CLI not found on PATH."
3728
+ exit 1
3729
+ fi
3730
+
3731
+ if ! command -v tmux >/dev/null 2>&1; then
3732
+ log "ERROR: tmux not found on PATH."
3733
+ exit 1
3734
+ fi
3735
+
3736
+ if ! claude --version >/dev/null 2>&1; then
3737
+ log "ERROR: claude --version failed."
3738
+ exit 1
3739
+ fi
3740
+
3741
+ log "Starting local-CC channel 2 (port ${CHANNEL_2_PORT})..."
3742
+ log "claude version: $(claude --version 2>&1 | head -1)"
3743
+
3744
+ tmux kill-session -t "$SESSION" 2>/dev/null || true
3745
+
3746
+ tmux new-session -d -s "$SESSION" \\
3747
+ "SYNKRO_CHANNEL_PORT=${CHANNEL_2_PORT} claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
3748
+
3749
+ sleep 3
3750
+ if tmux has-session -t "$SESSION" 2>/dev/null; then
3751
+ tmux send-keys -t "$SESSION" '1' 2>/dev/null || true
3752
+ sleep 1
3753
+ tmux send-keys -t "$SESSION" Enter 2>/dev/null || true
3754
+ sleep 1
3755
+ tmux send-keys -t "$SESSION" Enter 2>/dev/null || true
3756
+ log "Sent auto-accept keys to channel 2 session."
3757
+ fi
3758
+
3759
+ sleep 2
3760
+ if ! tmux has-session -t "$SESSION" 2>/dev/null; then
3761
+ log "ERROR: tmux session died immediately. Check $LOG for details."
3762
+ exit 1
3763
+ fi
3764
+
3765
+ log "tmux session started successfully (port ${CHANNEL_2_PORT})."
3766
+
3767
+ while tmux has-session -t "$SESSION" 2>/dev/null; do
3768
+ sleep 5
3769
+ done
3770
+
3465
3771
  log "tmux session ended."
3466
3772
  `;
3467
3773
  MCP_SERVER_NAME = "synkro-local";
@@ -3526,10 +3832,10 @@ function statusName(s) {
3526
3832
  }
3527
3833
  return "unknown";
3528
3834
  }
3529
- function findTask() {
3835
+ function findTask(channel = CHANNEL_PRIMARY) {
3530
3836
  const data = statusJson();
3531
3837
  for (const [id, t] of Object.entries(data.tasks)) {
3532
- if (t.label === TASK_LABEL) {
3838
+ if (t.label === channel.taskLabel) {
3533
3839
  return {
3534
3840
  id: Number(id),
3535
3841
  label: t.label,
@@ -3542,8 +3848,9 @@ function findTask() {
3542
3848
  return null;
3543
3849
  }
3544
3850
  function startTask(opts = {}) {
3545
- const cwd = opts.cwd ?? SESSION_DIR2;
3546
- const existing = findTask();
3851
+ const ch = opts.channel ?? CHANNEL_PRIMARY;
3852
+ const cwd = opts.cwd ?? ch.sessionDir;
3853
+ const existing = findTask(ch);
3547
3854
  if (existing) {
3548
3855
  spawnSync2("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
3549
3856
  }
@@ -3551,7 +3858,7 @@ function startTask(opts = {}) {
3551
3858
  const args2 = [
3552
3859
  "add",
3553
3860
  "--label",
3554
- TASK_LABEL,
3861
+ ch.taskLabel,
3555
3862
  "--working-directory",
3556
3863
  cwd,
3557
3864
  "--",
@@ -3562,27 +3869,28 @@ function startTask(opts = {}) {
3562
3869
  if (r.status !== 0) {
3563
3870
  throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
3564
3871
  }
3565
- const created = findTask();
3872
+ const created = findTask(ch);
3566
3873
  if (!created) {
3567
- throw new PueueError(`pueue add succeeded but no task with label ${TASK_LABEL} found`);
3874
+ throw new PueueError(`pueue add succeeded but no task with label ${ch.taskLabel} found`);
3568
3875
  }
3569
3876
  return created;
3570
3877
  }
3571
- function stopTask() {
3572
- spawnSync2("tmux", ["kill-session", "-t", TMUX_SESSION], { encoding: "utf-8" });
3573
- const t = findTask();
3878
+ function stopTask(channel = CHANNEL_PRIMARY) {
3879
+ spawnSync2("tmux", ["kill-session", "-t", channel.tmuxSession], { encoding: "utf-8" });
3880
+ const t = findTask(channel);
3574
3881
  if (!t) return;
3575
3882
  spawnSync2("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
3576
3883
  spawnSync2("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
3577
3884
  }
3578
- function tailLogs(lines = 80) {
3579
- const t = findTask();
3580
- if (!t) return "(no synkro local-cc task)";
3885
+ function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
3886
+ const t = findTask(channel);
3887
+ if (!t) return `(no ${channel.taskLabel} task)`;
3581
3888
  const r = spawnSync2("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
3582
3889
  return r.stdout || r.stderr || "(no output)";
3583
3890
  }
3584
3891
  function ensureRunning(opts = {}) {
3585
- const t = findTask();
3892
+ const ch = opts.channel ?? CHANNEL_PRIMARY;
3893
+ const t = findTask(ch);
3586
3894
  if (t && t.status === "Running") return t;
3587
3895
  return startTask(opts);
3588
3896
  }
@@ -3601,15 +3909,15 @@ function probePort(host, port, timeoutMs = 500) {
3601
3909
  sock.setTimeout(timeoutMs, () => done(false));
3602
3910
  });
3603
3911
  }
3604
- function tmuxDismissPrompts() {
3605
- spawnSync2("tmux", ["send-keys", "-t", TMUX_SESSION, "1"], { encoding: "utf-8" });
3606
- spawnSync2("tmux", ["send-keys", "-t", TMUX_SESSION, "Enter"], { encoding: "utf-8" });
3912
+ function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
3913
+ spawnSync2("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
3914
+ spawnSync2("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
3607
3915
  }
3608
- async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1") {
3916
+ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
3609
3917
  const deadline = Date.now() + timeoutMs;
3610
3918
  while (Date.now() < deadline) {
3611
3919
  if (await probePort(host, port)) return true;
3612
- tmuxDismissPrompts();
3920
+ tmuxDismissPrompts(tmuxSession);
3613
3921
  await new Promise((r) => setTimeout(r, 1e3));
3614
3922
  }
3615
3923
  return probePort(host, port);
@@ -3642,6 +3950,7 @@ function assertPueueInstalled() {
3642
3950
  throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
3643
3951
  }
3644
3952
  }
3953
+ spawnSync2("pueue", ["parallel", "2"], { encoding: "utf-8" });
3645
3954
  }
3646
3955
  function assertClaudeInstalled() {
3647
3956
  const r = spawnSync2("claude", ["--version"], { encoding: "utf-8" });
@@ -3660,13 +3969,16 @@ function assertTmuxInstalled() {
3660
3969
  }
3661
3970
  }
3662
3971
  }
3663
- var TASK_LABEL, TMUX_SESSION, SESSION_DIR2, PueueError;
3972
+ var TASK_LABEL, TMUX_SESSION, SESSION_DIR2, TASK_LABEL_2, TMUX_SESSION_2, SESSION_DIR_22, PueueError, CHANNEL_PRIMARY, CHANNEL_SECONDARY;
3664
3973
  var init_pueue = __esm({
3665
3974
  "cli/local-cc/pueue.ts"() {
3666
3975
  "use strict";
3667
3976
  TASK_LABEL = "synkro-local-cc";
3668
3977
  TMUX_SESSION = "synkro-local-cc";
3669
3978
  SESSION_DIR2 = join8(homedir7(), ".synkro", "cc_sessions");
3979
+ TASK_LABEL_2 = "synkro-local-cc-2";
3980
+ TMUX_SESSION_2 = "synkro-local-cc-2";
3981
+ SESSION_DIR_22 = join8(homedir7(), ".synkro", "cc_sessions_2");
3670
3982
  PueueError = class extends Error {
3671
3983
  constructor(message, cause) {
3672
3984
  super(message);
@@ -3675,6 +3987,8 @@ var init_pueue = __esm({
3675
3987
  }
3676
3988
  cause;
3677
3989
  };
3990
+ CHANNEL_PRIMARY = { taskLabel: TASK_LABEL, tmuxSession: TMUX_SESSION, sessionDir: SESSION_DIR2 };
3991
+ CHANNEL_SECONDARY = { taskLabel: TASK_LABEL_2, tmuxSession: TMUX_SESSION_2, sessionDir: SESSION_DIR_22 };
3678
3992
  }
3679
3993
  });
3680
3994
 
@@ -3702,7 +4016,7 @@ async function fetchPrimers() {
3702
4016
  }
3703
4017
  async function getPrimer(role) {
3704
4018
  const prompts = await fetchPrimers();
3705
- const primer = role === "grade-edit" ? prompts.grader_primer_edit : role === "grade-plan" ? prompts.grader_primer_plan : prompts.grader_primer_bash;
4019
+ const primer = role === "grade-edit" ? prompts.grader_primer_edit : role === "grade-plan" ? prompts.grader_primer_plan : role === "grade-cwe" ? prompts.grader_primer_cwe : prompts.grader_primer_bash;
3706
4020
  if (!primer) {
3707
4021
  throw new Error(`No primer for role "${role}" returned from API.`);
3708
4022
  }
@@ -3868,12 +4182,13 @@ async function submitToChannel(role, payload, opts = {}) {
3868
4182
  const content = await buildChannelContent(role, payload);
3869
4183
  const body = JSON.stringify({ role, content });
3870
4184
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
4185
+ const port = opts.port ?? CHANNEL_PORT;
3871
4186
  const startedAt = Date.now();
3872
4187
  try {
3873
4188
  const result = await new Promise((resolve2, reject) => {
3874
4189
  const req = httpRequest({
3875
4190
  host: CHANNEL_HOST,
3876
- port: CHANNEL_PORT,
4191
+ port,
3877
4192
  method: "POST",
3878
4193
  path: "/submit",
3879
4194
  headers: {
@@ -3921,9 +4236,9 @@ async function submitToChannel(role, payload, opts = {}) {
3921
4236
  throw err;
3922
4237
  }
3923
4238
  }
3924
- function isChannelAvailable(timeoutMs = 500) {
4239
+ function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
3925
4240
  return new Promise((resolve2) => {
3926
- const sock = connect2(CHANNEL_PORT, CHANNEL_HOST);
4241
+ const sock = connect2(port, CHANNEL_HOST);
3927
4242
  const done = (ok) => {
3928
4243
  try {
3929
4244
  sock.destroy();
@@ -4012,10 +4327,12 @@ function writeHookScripts() {
4012
4327
  const editCaptureScriptPath = join11(HOOKS_DIR, "cc-edit-capture.sh");
4013
4328
  const editPrecheckScriptPath = join11(HOOKS_DIR, "cc-edit-precheck.sh");
4014
4329
  const cveScanScriptPath = join11(HOOKS_DIR, "cc-cve-scan.sh");
4330
+ const cweScanScriptPath = join11(HOOKS_DIR, "cc-cwe-scan.sh");
4015
4331
  const planJudgeScriptPath = join11(HOOKS_DIR, "cc-plan-judge.sh");
4016
4332
  const stopSummaryScriptPath = join11(HOOKS_DIR, "cc-stop-summary.sh");
4017
4333
  const sessionStartScriptPath = join11(HOOKS_DIR, "cc-session-start.sh");
4018
4334
  const transcriptSyncScriptPath = join11(HOOKS_DIR, "cc-transcript-sync.sh");
4335
+ const userPromptSubmitScriptPath = join11(HOOKS_DIR, "cc-user-prompt-submit.sh");
4019
4336
  const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
4020
4337
  const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.sh");
4021
4338
  const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.sh");
@@ -4026,10 +4343,12 @@ function writeHookScripts() {
4026
4343
  writeFileSync7(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
4027
4344
  writeFileSync7(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
4028
4345
  writeFileSync7(cveScanScriptPath, CC_CVE_SCAN_SCRIPT, "utf-8");
4346
+ writeFileSync7(cweScanScriptPath, CC_CWE_SCAN_SCRIPT, "utf-8");
4029
4347
  writeFileSync7(planJudgeScriptPath, CC_PLAN_JUDGE_SCRIPT, "utf-8");
4030
4348
  writeFileSync7(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
4031
4349
  writeFileSync7(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
4032
4350
  writeFileSync7(transcriptSyncScriptPath, CC_TRANSCRIPT_SYNC_SCRIPT, "utf-8");
4351
+ writeFileSync7(userPromptSubmitScriptPath, CC_USER_PROMPT_SUBMIT_SCRIPT, "utf-8");
4033
4352
  writeFileSync7(commonScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
4034
4353
  writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_SCRIPT, "utf-8");
4035
4354
  writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_SCRIPT, "utf-8");
@@ -4040,10 +4359,12 @@ function writeHookScripts() {
4040
4359
  chmodSync2(editCaptureScriptPath, 493);
4041
4360
  chmodSync2(editPrecheckScriptPath, 493);
4042
4361
  chmodSync2(cveScanScriptPath, 493);
4362
+ chmodSync2(cweScanScriptPath, 493);
4043
4363
  chmodSync2(planJudgeScriptPath, 493);
4044
4364
  chmodSync2(stopSummaryScriptPath, 493);
4045
4365
  chmodSync2(sessionStartScriptPath, 493);
4046
4366
  chmodSync2(transcriptSyncScriptPath, 493);
4367
+ chmodSync2(userPromptSubmitScriptPath, 493);
4047
4368
  chmodSync2(commonScriptPath, 493);
4048
4369
  chmodSync2(cursorBashJudgePath, 493);
4049
4370
  chmodSync2(cursorEditPrecheckPath, 493);
@@ -4055,10 +4376,12 @@ function writeHookScripts() {
4055
4376
  editCaptureScript: editCaptureScriptPath,
4056
4377
  editPrecheckScript: editPrecheckScriptPath,
4057
4378
  cveScanScript: cveScanScriptPath,
4379
+ cweScanScript: cweScanScriptPath,
4058
4380
  planJudgeScript: planJudgeScriptPath,
4059
4381
  stopSummaryScript: stopSummaryScriptPath,
4060
4382
  sessionStartScript: sessionStartScriptPath,
4061
4383
  transcriptSyncScript: transcriptSyncScriptPath,
4384
+ userPromptSubmitScript: userPromptSubmitScriptPath,
4062
4385
  cursorBashJudgeScript: cursorBashJudgePath,
4063
4386
  cursorEditPrecheckScript: cursorEditPrecheckPath,
4064
4387
  cursorEditCaptureScript: cursorEditCapturePath,
@@ -4094,7 +4417,7 @@ function writeConfigEnv(opts) {
4094
4417
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
4095
4418
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
4096
4419
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
4097
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.42")}`
4420
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.45")}`
4098
4421
  ];
4099
4422
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
4100
4423
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -4288,6 +4611,11 @@ async function installCommand(opts = {}) {
4288
4611
  const ready = await waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST);
4289
4612
  if (ready) console.log(` channel ready at ${CHANNEL_HOST}:${CHANNEL_PORT}`);
4290
4613
  else console.warn(" \u26A0 channel did not come up within 60s \u2014 check `synkro local-cc logs`");
4614
+ const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
4615
+ console.log(` CWE channel: id=${t2.id} status=${t2.status}`);
4616
+ const ready2 = await waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession);
4617
+ if (ready2) console.log(` CWE channel ready at ${CHANNEL_HOST}:${CHANNEL_2_PORT}`);
4618
+ else console.warn(" \u26A0 CWE channel did not come up within 60s");
4291
4619
  updateLocalInferenceFlag(true);
4292
4620
  } catch (err) {
4293
4621
  console.warn(` \u26A0 Local-CC setup skipped: ${err.message}`);
@@ -4398,10 +4726,12 @@ async function installCommand(opts = {}) {
4398
4726
  editCaptureScriptPath: scripts.editCaptureScript,
4399
4727
  editPrecheckScriptPath: scripts.editPrecheckScript,
4400
4728
  cveScanScriptPath: scripts.cveScanScript,
4729
+ cweScanScriptPath: scripts.cweScanScript,
4401
4730
  planJudgeScriptPath: scripts.planJudgeScript,
4402
4731
  stopSummaryScriptPath: scripts.stopSummaryScript,
4403
4732
  sessionStartScriptPath: scripts.sessionStartScript,
4404
4733
  transcriptSyncScriptPath: scripts.transcriptSyncScript,
4734
+ userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
4405
4735
  skipTranscriptSync: !transcriptConsent
4406
4736
  });
4407
4737
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
@@ -4542,6 +4872,24 @@ async function installCommand(opts = {}) {
4542
4872
  } catch {
4543
4873
  }
4544
4874
  console.warn(` Run \`synkro local-cc status\` and \`synkro local-cc logs --tmux\` to debug.
4875
+ `);
4876
+ }
4877
+ const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
4878
+ console.log(`Local-CC CWE channel: id=${t2.id} status=${t2.status}`);
4879
+ console.log("Waiting for CWE channel (up to 60s)...");
4880
+ const ready2 = await waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession);
4881
+ if (ready2) {
4882
+ console.log(` CWE channel ready at ${CHANNEL_HOST}:${CHANNEL_2_PORT}`);
4883
+ try {
4884
+ console.log(" warming up CWE inference...");
4885
+ await submitToChannel("grade-cwe", 'File: /tmp/warmup.ts\nContent (first 4000 chars):\nconsole.log("hello");\n\nCWE rules to check against:\n[]\n', { timeoutMs: 3e4, port: CHANNEL_2_PORT });
4886
+ console.log(" CWE inference warm\n");
4887
+ } catch {
4888
+ console.log(" CWE warmup skipped (non-fatal)\n");
4889
+ }
4890
+ } else {
4891
+ console.warn(` \u26A0 CWE channel did not come up within 60s.`);
4892
+ console.warn(` Run \`synkro local-cc status\` to debug.
4545
4893
  `);
4546
4894
  }
4547
4895
  } catch (err) {
@@ -6173,17 +6521,26 @@ async function cmdStatus() {
6173
6521
  console.log(`Pueue: NOT AVAILABLE (${err.message})`);
6174
6522
  return;
6175
6523
  }
6176
- const t = findTask();
6524
+ const t = findTask(CHANNEL_PRIMARY);
6177
6525
  if (!t) {
6178
- console.log("Pueue task: not present");
6526
+ console.log("Channel 1 (judge) pueue task: not present");
6527
+ } else {
6528
+ console.log(`Channel 1 (judge) pueue task: id=${t.id} status=${t.status}`);
6529
+ }
6530
+ const ch1Up = await isChannelAvailable();
6531
+ console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
6532
+ const tmux1 = spawnSync3("tmux", ["has-session", "-t", TMUX_SESSION_NAME], { encoding: "utf-8" });
6533
+ console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
6534
+ const t2 = findTask(CHANNEL_SECONDARY);
6535
+ if (!t2) {
6536
+ console.log("Channel 2 (CWE) pueue task: not present");
6179
6537
  } else {
6180
- console.log(`Pueue task: id=${t.id} status=${t.status} cwd=${t.cwd}`);
6181
- console.log(` command: ${t.command}`);
6538
+ console.log(`Channel 2 (CWE) pueue task: id=${t2.id} status=${t2.status}`);
6182
6539
  }
6183
- const channelUp = await isChannelAvailable();
6184
- console.log(`Channel ${CHANNEL_HOST}:${CHANNEL_PORT}: ${channelUp ? "reachable" : "unreachable"}`);
6185
- const tmuxCheck = spawnSync3("tmux", ["has-session", "-t", TMUX_SESSION_NAME], { encoding: "utf-8" });
6186
- console.log(`tmux session '${TMUX_SESSION_NAME}': ${tmuxCheck.status === 0 ? "live" : "absent"}`);
6540
+ const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
6541
+ console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
6542
+ const tmux2 = spawnSync3("tmux", ["has-session", "-t", TMUX_SESSION_NAME_2], { encoding: "utf-8" });
6543
+ console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
6187
6544
  }
6188
6545
  async function cmdEnable() {
6189
6546
  assertClaudeInstalled();
@@ -6193,13 +6550,21 @@ async function cmdEnable() {
6193
6550
  const r = installLocalCC();
6194
6551
  console.log(` plugin: ${r.pluginPath}`);
6195
6552
  console.log(` cwd: ${r.sessionDir}`);
6196
- console.log("Starting pueue task...");
6197
- const t = ensureRunning();
6198
- console.log(` task: id=${t.id} status=${t.status}`);
6199
- console.log("Waiting for channel (auto-confirming any CC prompts)...");
6200
- const ready = await waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST);
6201
- if (ready) console.log(` channel ready at ${CHANNEL_HOST}:${CHANNEL_PORT}`);
6202
- else console.warn(` \u26A0 channel did not come up within 60s \u2014 check \`synkro local-cc logs\``);
6553
+ console.log("Starting channel 1 (judge)...");
6554
+ const t1 = ensureRunning({ channel: CHANNEL_PRIMARY });
6555
+ console.log(` task: id=${t1.id} status=${t1.status}`);
6556
+ console.log("Starting channel 2 (CWE)...");
6557
+ const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
6558
+ console.log(` task: id=${t2.id} status=${t2.status}`);
6559
+ console.log("Waiting for channels (auto-confirming any CC prompts)...");
6560
+ const [ready1, ready2] = await Promise.all([
6561
+ waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
6562
+ waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
6563
+ ]);
6564
+ if (ready1) console.log(` channel 1 ready at ${CHANNEL_HOST}:${CHANNEL_PORT}`);
6565
+ else console.warn(` \u26A0 channel 1 did not come up within 60s \u2014 check \`synkro local-cc logs\``);
6566
+ if (ready2) console.log(` channel 2 ready at ${CHANNEL_HOST}:${CHANNEL_2_PORT}`);
6567
+ else console.warn(` \u26A0 channel 2 (CWE) did not come up within 60s`);
6203
6568
  console.log("Updating inference settings...");
6204
6569
  await setServerGradingProvider("claude-code");
6205
6570
  updateLocalInferenceFlag2(true);
@@ -6211,25 +6576,58 @@ async function cmdDisable() {
6211
6576
  updateLocalInferenceFlag2(false);
6212
6577
  console.log("Grading provider cleared (remote inference restored). Pueue task left running \u2014 use `synkro local-cc stop` to terminate.");
6213
6578
  }
6579
+ async function warmChannels(ready1, ready2) {
6580
+ const warmups = [];
6581
+ if (ready1) {
6582
+ warmups.push(
6583
+ submitToChannel("grade-bash", "Proposed command: echo hello\nUser intent: warmup\nRecent user messages: []\nRecent actions: []\nOrg rules: []\n", { timeoutMs: 3e4 }).then(() => console.log(" channel 1 warm.")).catch(() => console.log(" channel 1 warmup skipped (non-fatal)."))
6584
+ );
6585
+ }
6586
+ if (ready2) {
6587
+ warmups.push(
6588
+ submitToChannel("grade-cwe", 'File: /tmp/warmup.ts\nContent (first 4000 chars):\nconsole.log("hello");\n\nCWE rules to check against:\n[]\n', { timeoutMs: 3e4, port: CHANNEL_2_PORT }).then(() => console.log(" channel 2 warm.")).catch(() => console.log(" channel 2 warmup skipped (non-fatal)."))
6589
+ );
6590
+ }
6591
+ if (warmups.length) {
6592
+ console.log("Warming up inference...");
6593
+ await Promise.all(warmups);
6594
+ }
6595
+ }
6214
6596
  async function cmdStart() {
6215
6597
  assertClaudeInstalled();
6216
6598
  assertPueueInstalled();
6217
6599
  assertTmuxInstalled();
6218
- const t = ensureRunning();
6219
- console.log(`Pueue task: id=${t.id} status=${t.status}`);
6220
- const ready = await waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST);
6221
- console.log(ready ? "channel ready." : "\u26A0 channel did not come up within 60s.");
6600
+ const t1 = ensureRunning({ channel: CHANNEL_PRIMARY });
6601
+ console.log(`Channel 1 (judge): id=${t1.id} status=${t1.status}`);
6602
+ const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
6603
+ console.log(`Channel 2 (CWE): id=${t2.id} status=${t2.status}`);
6604
+ const [ready1, ready2] = await Promise.all([
6605
+ waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
6606
+ waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
6607
+ ]);
6608
+ console.log(ready1 ? `channel 1 ready (${CHANNEL_PORT}).` : "\u26A0 channel 1 did not come up within 60s.");
6609
+ console.log(ready2 ? `channel 2 ready (${CHANNEL_2_PORT}).` : "\u26A0 channel 2 (CWE) did not come up within 60s.");
6610
+ await warmChannels(ready1, ready2);
6222
6611
  }
6223
6612
  function cmdStop() {
6224
- stopTask();
6225
- console.log("Pueue task stopped.");
6613
+ stopTask(CHANNEL_PRIMARY);
6614
+ stopTask(CHANNEL_SECONDARY);
6615
+ console.log("Both channels stopped.");
6226
6616
  }
6227
6617
  async function cmdRestart() {
6228
- stopTask();
6229
- const t = startTask();
6230
- console.log(`Pueue task restarted: id=${t.id} status=${t.status}`);
6231
- const ready = await waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST);
6232
- console.log(ready ? "channel ready." : "\u26A0 channel did not come up within 60s.");
6618
+ stopTask(CHANNEL_PRIMARY);
6619
+ stopTask(CHANNEL_SECONDARY);
6620
+ const t1 = startTask({ channel: CHANNEL_PRIMARY });
6621
+ const t2 = startTask({ channel: CHANNEL_SECONDARY });
6622
+ console.log(`Channel 1 restarted: id=${t1.id} status=${t1.status}`);
6623
+ console.log(`Channel 2 restarted: id=${t2.id} status=${t2.status}`);
6624
+ const [ready1, ready2] = await Promise.all([
6625
+ waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
6626
+ waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
6627
+ ]);
6628
+ console.log(ready1 ? `channel 1 ready (${CHANNEL_PORT}).` : "\u26A0 channel 1 did not come up within 60s.");
6629
+ console.log(ready2 ? `channel 2 ready (${CHANNEL_2_PORT}).` : "\u26A0 channel 2 (CWE) did not come up within 60s.");
6630
+ await warmChannels(ready1, ready2);
6233
6631
  }
6234
6632
  function relativeTime(iso) {
6235
6633
  const ts = new Date(iso).getTime();
@@ -6445,8 +6843,9 @@ async function gradeCommand(args2) {
6445
6843
  if (mode === "edit") role = "grade-edit";
6446
6844
  else if (mode === "bash") role = "grade-bash";
6447
6845
  else if (mode === "plan") role = "grade-plan";
6846
+ else if (mode === "cwe") role = "grade-cwe";
6448
6847
  else {
6449
- console.error("Usage: synkro grade <edit|bash|plan>");
6848
+ console.error("Usage: synkro grade <edit|bash|plan|cwe>");
6450
6849
  process.exit(2);
6451
6850
  }
6452
6851
  const payload = await readStdin();