@jonit-dev/night-watch-cli 1.8.4-beta.4 → 1.8.4-beta.6

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.
@@ -102,24 +102,12 @@ latest_failure_detail() {
102
102
  }
103
103
 
104
104
  # ── Global Job Queue Gate ────────────────────────────────────────────────────
105
- # Acquire global gate before per-project lock to serialize jobs across projects.
106
- # When gate is busy, enqueue the job and exit cleanly.
105
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
107
106
  if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
108
107
  if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
109
108
  arm_global_queue_cleanup
110
- elif acquire_global_gate; then
111
- if queue_can_start_now; then
112
- arm_global_queue_cleanup
113
- else
114
- release_global_gate
115
- enqueue_job "${SCRIPT_TYPE}" "${PROJECT_DIR}"
116
- emit_result "queued"
117
- exit 0
118
- fi
119
109
  else
120
- enqueue_job "${SCRIPT_TYPE}" "${PROJECT_DIR}"
121
- emit_result "queued"
122
- exit 0
110
+ claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
123
111
  fi
124
112
  fi
125
113
  # ──────────────────────────────────────────────────────────────────────────────
@@ -1029,55 +1029,13 @@ find_eligible_board_issue() {
1029
1029
 
1030
1030
  # ── Global Job Queue Gate ─────────────────────────────────────────────────────
1031
1031
 
1032
- # Get the path to the queue lock file
1033
- get_queue_lock_path() {
1034
- local queue_home="${NIGHT_WATCH_HOME:-${HOME}/.night-watch}"
1035
- printf "%s/%s" "${queue_home}" "queue.lock"
1036
- }
1037
-
1038
- # Try to acquire the global queue gate using flock.
1039
- # Uses a shared lock file (~/.night-watch/queue.lock).
1040
- # Returns 0 if acquired (gate is free), 1 if busy.
1041
- # Holds the lock fd open for the caller via GLOBAL_GATE_FD variable.
1042
- acquire_global_gate() {
1043
- local lock_path
1044
- lock_path=$(get_queue_lock_path)
1045
-
1046
- # Ensure the .night-watch directory exists
1047
- mkdir -p "$(dirname "${lock_path}")"
1048
-
1049
- # Open the lock file as fd 200
1050
- exec 200>"${lock_path}"
1051
-
1052
- # Try non-blocking flock
1053
- if flock --nonblock 200; then
1054
- GLOBAL_GATE_FD=200
1055
- return 0
1056
- else
1057
- GLOBAL_GATE_FD=""
1058
- return 1
1059
- fi
1060
- }
1061
-
1062
- # Release the global queue gate
1063
- release_global_gate() {
1064
- if [ -n "${GLOBAL_GATE_FD:-}" ]; then
1065
- flock --unlock "${GLOBAL_GATE_FD}" 2>/dev/null || true
1066
- exec 200>&-
1067
- GLOBAL_GATE_FD=""
1068
- fi
1069
- }
1070
-
1071
1032
  __night_watch_queue_cleanup() {
1072
1033
  local exit_code="${1:-0}"
1073
-
1074
1034
  if [ "${NW_QUEUE_CLEANUP_ARMED:-0}" = "1" ]; then
1075
1035
  NW_QUEUE_CLEANUP_ARMED=0
1076
1036
  complete_queued_job
1077
1037
  dispatch_next_queued_job
1078
- release_global_gate
1079
1038
  fi
1080
-
1081
1039
  return "${exit_code}"
1082
1040
  }
1083
1041
 
@@ -1090,6 +1048,35 @@ arm_global_queue_cleanup() {
1090
1048
  append_exit_trap "__night_watch_queue_cleanup \$?"
1091
1049
  }
1092
1050
 
1051
+ # Atomically claim a queue slot or enqueue for later dispatch.
1052
+ # Uses DB transaction (via `queue claim` CLI) for atomicity — no flock needed.
1053
+ # Sets NW_QUEUE_ENTRY_ID on success and arms the cleanup trap.
1054
+ # Calls enqueue_job and exits 0 if no slot is available.
1055
+ claim_or_enqueue() {
1056
+ local script_type="${1:?script_type required}"
1057
+ local project_dir="${2:?project_dir required}"
1058
+ local provider_key
1059
+ provider_key=$(resolve_provider_key "${project_dir}" "${script_type}")
1060
+
1061
+ local cli_bin
1062
+ cli_bin=$(resolve_night_watch_cli) || {
1063
+ log "ERROR: Cannot resolve night-watch CLI for claim"
1064
+ return 1
1065
+ }
1066
+
1067
+ local claim_id
1068
+ if claim_id=$("${cli_bin}" queue claim "${script_type}" "${project_dir}" --provider-key "${provider_key}" 2>/dev/null); then
1069
+ NW_QUEUE_ENTRY_ID="${claim_id}"
1070
+ export NW_QUEUE_ENTRY_ID
1071
+ arm_global_queue_cleanup
1072
+ return 0
1073
+ else
1074
+ enqueue_job "${script_type}" "${project_dir}"
1075
+ emit_result "queued"
1076
+ exit 0
1077
+ fi
1078
+ }
1079
+
1093
1080
  # Enqueue the current job to the SQLite queue.
1094
1081
  # Usage: enqueue_job <job_type> <project_dir>
1095
1082
  # Stores project path/name, job type, and relevant NW_* env vars for later dispatch.
@@ -1115,7 +1102,12 @@ enqueue_job() {
1115
1102
 
1116
1103
  log "QUEUE: Enqueueing ${job_type} for ${project_name} (gate busy)"
1117
1104
 
1118
- "${cli_bin}" queue enqueue "${job_type}" "${project_dir}" --env "${env_json}" >> "${LOG_FILE:-/dev/null}" 2>&1 || {
1105
+ local provider_key_arg=()
1106
+ if [ -n "${NW_PROVIDER_KEY:-}" ]; then
1107
+ provider_key_arg=(--provider-key "${NW_PROVIDER_KEY}")
1108
+ fi
1109
+
1110
+ "${cli_bin}" queue enqueue "${job_type}" "${project_dir}" --env "${env_json}" "${provider_key_arg[@]}" >> "${LOG_FILE:-/dev/null}" 2>&1 || {
1119
1111
  log "WARN: Failed to enqueue job"
1120
1112
  return 1
1121
1113
  }
@@ -1123,6 +1115,18 @@ enqueue_job() {
1123
1115
  return 0
1124
1116
  }
1125
1117
 
1118
+ # Resolve the provider bucket key for a given project and job type.
1119
+ # Uses the night-watch CLI to compute the canonical bucket key (e.g. claude-native, codex).
1120
+ # Prints the key to stdout, or an empty string if it cannot be resolved.
1121
+ # Usage: resolve_provider_key <project_dir> <job_type>
1122
+ resolve_provider_key() {
1123
+ local project_dir="${1:?project_dir required}"
1124
+ local job_type="${2:?job_type required}"
1125
+ local cli_bin
1126
+ cli_bin=$(resolve_night_watch_cli) || { printf ""; return 0; }
1127
+ "${cli_bin}" queue resolve-key --project "${project_dir}" --job-type "${job_type}" 2>/dev/null || printf ""
1128
+ }
1129
+
1126
1130
  # Dispatch the next queued job after the current job completes.
1127
1131
  # Picks highest-priority pending job, marks it dispatched, and spawns it.
1128
1132
  dispatch_next_queued_job() {
@@ -1143,16 +1147,6 @@ dispatch_next_queued_job() {
1143
1147
  "${cli_bin}" queue dispatch --log "${LOG_FILE:-/dev/null}" 2>/dev/null || true
1144
1148
  }
1145
1149
 
1146
- queue_can_start_now() {
1147
- local cli_bin
1148
- cli_bin=$(resolve_night_watch_cli) || {
1149
- log "WARN: Cannot resolve night-watch CLI for queue slot check"
1150
- return 0
1151
- }
1152
-
1153
- "${cli_bin}" queue can-start >/dev/null 2>&1
1154
- }
1155
-
1156
1150
  complete_queued_job() {
1157
1151
  local queue_entry_id="${NW_QUEUE_ENTRY_ID:-}"
1158
1152
  if [ -z "${queue_entry_id}" ]; then
@@ -17,10 +17,12 @@ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
17
17
  PROJECT_NAME=$(basename "${PROJECT_DIR}")
18
18
  LOG_DIR="${PROJECT_DIR}/logs"
19
19
  LOG_FILE="${LOG_DIR}/plan.log"
20
+ LOCK_FILE=""
20
21
  MAX_RUNTIME="${NW_PLAN_MAX_RUNTIME:-1800}"
21
22
  MAX_LOG_SIZE="524288" # 512 KB
22
23
  PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
23
24
  PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
25
+ SCRIPT_TYPE="planner"
24
26
  SCRIPT_START_TIME=$(date +%s)
25
27
 
26
28
  mkdir -p "${LOG_DIR}"
@@ -53,6 +55,8 @@ if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
53
55
  exit 1
54
56
  fi
55
57
 
58
+ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
59
+ LOCK_FILE="/tmp/night-watch-plan-${PROJECT_RUNTIME_KEY}.lock"
56
60
  PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
57
61
  PRD_DIR="${NW_PRD_DIR:-docs/PRDs}"
58
62
  PLAN_TASK="${NW_PLAN_TASK:-}"
@@ -62,6 +66,25 @@ log_separator
62
66
  log "RUN-START: planner invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} dry_run=${NW_DRY_RUN:-0}"
63
67
  log "CONFIG: max_runtime=${MAX_RUNTIME}s prd_dir=${PRD_DIR}"
64
68
 
69
+ if ! acquire_lock "${LOCK_FILE}"; then
70
+ exit 0
71
+ fi
72
+ # ── Global Job Queue Gate ────────────────────────────────────────────────────
73
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
74
+ if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
75
+ if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
76
+ arm_global_queue_cleanup
77
+ else
78
+ claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
79
+ fi
80
+ fi
81
+ # ──────────────────────────────────────────────────────────────────────────────
82
+ cleanup_on_exit() {
83
+ rm -f "${LOCK_FILE}"
84
+ }
85
+
86
+ trap cleanup_on_exit EXIT
87
+
65
88
  # Dry-run mode
66
89
  if [ "${NW_DRY_RUN:-0}" = "1" ]; then
67
90
  echo "=== Dry Run: PRD Planner ==="
@@ -82,9 +82,6 @@ else
82
82
  LOCK_FILE="${GLOBAL_LOCK_FILE}"
83
83
  fi
84
84
 
85
- # ── Global Job Queue Gate ────────────────────────────────────────────────────
86
- # Acquire global gate before per-project lock to serialize jobs across projects.
87
- # When gate is busy, enqueue the job and exit cleanly.
88
85
  SCRIPT_TYPE="reviewer"
89
86
 
90
87
  emit_result() {
@@ -106,24 +103,12 @@ extract_review_score_from_text() {
106
103
  }
107
104
 
108
105
  # ── Global Job Queue Gate ────────────────────────────────────────────────────
109
- # Acquire global gate before per-project lock to serialize jobs across projects.
110
- # When gate is busy, enqueue the job and exit cleanly.
106
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
111
107
  if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
112
108
  if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
113
109
  arm_global_queue_cleanup
114
- elif acquire_global_gate; then
115
- if queue_can_start_now; then
116
- arm_global_queue_cleanup
117
- else
118
- release_global_gate
119
- enqueue_job "${SCRIPT_TYPE}" "${PROJECT_DIR}"
120
- emit_result "queued"
121
- exit 0
122
- fi
123
110
  else
124
- enqueue_job "${SCRIPT_TYPE}" "${PROJECT_DIR}"
125
- emit_result "queued"
126
- exit 0
111
+ claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
127
112
  fi
128
113
  fi
129
114
  # ──────────────────────────────────────────────────────────────────────────────
@@ -56,24 +56,12 @@ emit_result() {
56
56
  }
57
57
 
58
58
  # ── Global Job Queue Gate ────────────────────────────────────────────────────
59
- # Acquire global gate before per-project lock to serialize jobs across projects.
60
- # When gate is busy, enqueue the job and exit cleanly.
59
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
61
60
  if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
62
61
  if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
63
62
  arm_global_queue_cleanup
64
- elif acquire_global_gate; then
65
- if queue_can_start_now; then
66
- arm_global_queue_cleanup
67
- else
68
- release_global_gate
69
- enqueue_job "${SCRIPT_TYPE}" "${PROJECT_DIR}"
70
- emit_result "queued"
71
- exit 0
72
- fi
73
63
  else
74
- enqueue_job "${SCRIPT_TYPE}" "${PROJECT_DIR}"
75
- emit_result "queued"
76
- exit 0
64
+ claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
77
65
  fi
78
66
  fi
79
67
  # ──────────────────────────────────────────────────────────────────────────────
@@ -61,23 +61,13 @@ log "CONFIG: max_runtime=${MAX_RUNTIME}s roadmap_path=${NW_ROADMAP_PATH:-ROADMAP
61
61
  if ! acquire_lock "${LOCK_FILE}"; then
62
62
  exit 0
63
63
  fi
64
- # ── Global Job Queue Gate ──────────────────────────────────────
64
+ # ── Global Job Queue Gate ────────────────────────────────────────────────────
65
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
65
66
  if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
66
67
  if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
67
68
  arm_global_queue_cleanup
68
- elif acquire_global_gate; then
69
- if queue_can_start_now; then
70
- arm_global_queue_cleanup
71
- else
72
- release_global_gate
73
- enqueue_job "slicer" "${PROJECT_DIR}"
74
- emit_result "queued"
75
- exit 0
76
- fi
77
69
  else
78
- enqueue_job "slicer" "${PROJECT_DIR}"
79
- emit_result "queued"
80
- exit 0
70
+ claim_or_enqueue "slicer" "${PROJECT_DIR}"
81
71
  fi
82
72
  fi
83
73
  # ──────────────────────────────────────────────────────────────────────────────