@walwal-harness/cli 4.0.0-beta.9 → 5.0.0

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.
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
- # harness-monitor.sh — Agent Lifecycle Monitor
2
+ # harness-monitor.sh — Agent Lifecycle Monitor (v5 unified)
3
3
  #
4
- # v3 모드: 단일 이벤트 스트림 (progress.json 변경 감지 + audit.log)
5
- # v4 모드: 팀별 섹션 분리 (feature-queue.json + progress.log 기반)
4
+ # Solo 모드: 단일 이벤트 스트림 (progress.json 변경 감지 + audit.log)
5
+ # Team 모드: 팀별 섹션 분리 (feature-queue.json + progress.log 기반)
6
6
  #
7
7
  # Usage: bash scripts/harness-monitor.sh [project-root]
8
8
 
@@ -10,7 +10,19 @@ set -uo pipefail
10
10
 
11
11
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
12
 
13
- PROJECT_ROOT="${1:-}"
13
+ # ── Args ──
14
+ # Usage: harness-monitor.sh [project-root] [--team N]
15
+ # --team N → 단일 팀만 렌더 (tmux per-team pane 용도)
16
+ PROJECT_ROOT=""
17
+ TEAM_FILTER=""
18
+ while [ $# -gt 0 ]; do
19
+ case "$1" in
20
+ --team) TEAM_FILTER="$2"; shift 2 ;;
21
+ --team=*) TEAM_FILTER="${1#--team=}"; shift ;;
22
+ *) [ -z "$PROJECT_ROOT" ] && PROJECT_ROOT="$1"; shift ;;
23
+ esac
24
+ done
25
+
14
26
  if [ -z "$PROJECT_ROOT" ]; then
15
27
  source "$SCRIPT_DIR/lib/harness-render-progress.sh"
16
28
  PROJECT_ROOT="$(resolve_harness_root ".")" || {
@@ -50,7 +62,7 @@ team_color() {
50
62
  }
51
63
 
52
64
  # ══════════════════════════════════════════
53
- # v4 모드: 팀별 섹션 렌더링
65
+ # Team 모드: 팀별 섹션 렌더링
54
66
  # ══════════════════════════════════════════
55
67
 
56
68
  render_v4_header() {
@@ -58,8 +70,18 @@ render_v4_header() {
58
70
  echo ""
59
71
  }
60
72
 
73
+ # 고정폭 배너 구분선 (터미널 너비에 맞춰 채움)
74
+ banner_line() {
75
+ local cols
76
+ cols=$(tput cols 2>/dev/null || echo 78)
77
+ local line=""
78
+ for ((i=0; i<cols; i++)); do line+="━"; done
79
+ echo "$line"
80
+ }
81
+
61
82
  render_team_section() {
62
83
  local team_num="$1"
84
+ local log_lines="${2:-10}"
63
85
  local color
64
86
  color=$(team_color "$team_num")
65
87
 
@@ -90,93 +112,119 @@ render_team_section() {
90
112
  *) phase_str="" ;;
91
113
  esac
92
114
 
93
- # 헤더 라인
94
- printf "%b%b T%s%b " "$color" "$icon" "$team_num" "$RESET"
115
+ # ── 큰 배너 ──
116
+ local bar
117
+ bar=$(banner_line)
118
+ printf "%b%s%b\n" "$color" "$bar" "$RESET"
119
+
120
+ printf "%b%b TEAM %s%b" "$color$BOLD" "$icon" "$team_num" "$RESET"
95
121
  if [ "$t_feature" != "—" ] && [ "$t_feature" != "null" ]; then
96
- printf "%s " "$t_feature"
122
+ printf " %b%s%b" "$BOLD" "$t_feature" "$RESET"
97
123
  if [ -n "$phase_str" ]; then
98
- printf "%b%s%b" "$color" "$phase_str" "$RESET"
124
+ printf " %b[%s]%b" "$color" "$phase_str" "$RESET"
99
125
  fi
100
126
  if [ -n "$t_attempt" ] && [ "$t_attempt" != "—" ] && [ "$t_attempt" != "1" ]; then
101
- printf " %b#%s%b" "$DIM" "$t_attempt" "$RESET"
127
+ printf " %battempt #%s%b" "$YELLOW" "$t_attempt" "$RESET"
102
128
  fi
103
129
  else
104
- printf "%bidle%b" "$DIM" "$RESET"
130
+ printf " %bidle%b" "$DIM" "$RESET"
105
131
  fi
106
132
  echo ""
133
+ printf "%b%s%b\n" "$color" "$bar" "$RESET"
107
134
 
108
- # 팀 로그 (progress.log에서 team-N 엔트리만 추출)
135
+ # ── 팀 로그 ──
136
+ local have_logs=0
109
137
  if [ -f "$PROGRESS_LOG" ]; then
110
- grep -i "team-${team_num}\|team_${team_num}" "$PROGRESS_LOG" 2>/dev/null | tail -5 | while IFS= read -r line; do
111
- local ts action detail
112
- ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
113
- action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
114
- detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
115
-
116
- local short_ts
117
- short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
118
-
119
- if [ ${#detail} -gt 35 ]; then detail="${detail:0:33}.."; fi
120
-
121
- # 액션별 아이콘
122
- local a_icon="·" a_color="$DIM"
123
- case "$action" in
124
- gen) a_icon="▶"; a_color="$GREEN" ;;
125
- eval) a_icon="✦"; a_color="$BLUE" ;;
126
- pass) a_icon="✓"; a_color="$GREEN" ;;
127
- fail) a_icon="✗"; a_color="$RED" ;;
128
- dequeue) a_icon="→"; a_color="$CYAN" ;;
129
- gate) a_icon="◆"; a_color="$YELLOW" ;;
130
- *) a_icon="·"; a_color="$DIM" ;;
131
- esac
132
-
133
- printf " %b%s%b %b%s%b %b%s%b\n" \
134
- "$DIM" "$short_ts" "$RESET" \
135
- "$a_color" "$a_icon" "$RESET" \
136
- "$DIM" "$detail" "$RESET"
137
- done
138
+ local matched
139
+ matched=$(grep -E "team-${team_num}\b|team_${team_num}\b" "$PROGRESS_LOG" 2>/dev/null | tail -"$log_lines")
140
+ if [ -n "$matched" ]; then
141
+ have_logs=1
142
+ local cols
143
+ cols=$(tput cols 2>/dev/null || echo 78)
144
+ # 포맷: [ts] [icon agent] detail (너무 길면 터미널 너비에서 줄바꿈)
145
+ local prefix_w=18 # " HH:MM ✦ eval "
146
+ local detail_w=$(( cols - prefix_w ))
147
+ [ "$detail_w" -lt 20 ] && detail_w=20
148
+
149
+ echo "$matched" | while IFS= read -r line; do
150
+ local ts action detail
151
+ ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
152
+ action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
153
+ detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
154
+
155
+ local short_ts
156
+ short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
157
+
158
+ local a_icon="·" a_color="$DIM" a_label="$action"
159
+ case "$action" in
160
+ gen|gen-start|gen-read|gen-write|gen-test|gen-done)
161
+ a_icon="▶"; a_color="$GREEN"; a_label="Gen" ;;
162
+ eval|eval-start|eval-check|eval-done)
163
+ a_icon="✦"; a_color="$BLUE"; a_label="Eval" ;;
164
+ result|pass)
165
+ a_icon="✓"; a_color="$GREEN"; a_label="Result" ;;
166
+ fail)
167
+ a_icon="✗"; a_color="$RED"; a_label="Result" ;;
168
+ dequeue) a_icon="→"; a_color="$CYAN"; a_label="Queue" ;;
169
+ gate) a_icon="◆"; a_color="$YELLOW"; a_label="Gate" ;;
170
+ *) a_icon="·"; a_color="$DIM"; a_label="$action" ;;
171
+ esac
172
+
173
+ # 첫 줄
174
+ local first="${detail:0:$detail_w}"
175
+ printf " %b%s%b %b%s%b %-6s %s\n" \
176
+ "$DIM" "$short_ts" "$RESET" \
177
+ "$a_color" "$a_icon" "$RESET" \
178
+ "$a_label" "$first"
179
+ # 이어질 줄 (긴 detail 은 접지 않고 들여쓰기로 이어서 출력)
180
+ local rest="${detail:$detail_w}"
181
+ while [ -n "$rest" ]; do
182
+ local chunk="${rest:0:$detail_w}"
183
+ rest="${rest:$detail_w}"
184
+ printf " %*s%s\n" "$prefix_w" "" "$chunk"
185
+ done
186
+ done
187
+ fi
188
+ fi
189
+ if [ "$have_logs" -eq 0 ]; then
190
+ printf " %b(no activity)%b\n" "$DIM" "$RESET"
138
191
  fi
139
192
  }
140
193
 
141
194
  render_v4() {
195
+ # 단일 팀 모드 (tmux per-team pane)
196
+ if [ -n "$TEAM_FILTER" ]; then
197
+ local rows log_lines
198
+ rows=$(tput lines 2>/dev/null || echo 20)
199
+ log_lines=$(( rows - 5 ))
200
+ [ "$log_lines" -lt 3 ] && log_lines=3
201
+ render_team_section "$TEAM_FILTER" "$log_lines"
202
+ return
203
+ fi
204
+
142
205
  render_v4_header
143
206
 
144
- # 팀 수 확인
145
207
  local team_count=3
146
208
  if [ -f "$QUEUE" ]; then
147
209
  team_count=$(jq '.teams | length' "$QUEUE" 2>/dev/null || echo 3)
148
210
  fi
211
+ [ "$team_count" -lt 1 ] && team_count=3
212
+
213
+ local rows per_team log_lines
214
+ rows=$(tput lines 2>/dev/null || echo 40)
215
+ per_team=$(( (rows - 2) / team_count ))
216
+ log_lines=$(( per_team - 5 ))
217
+ if [ "$log_lines" -lt 3 ]; then log_lines=3; fi
218
+ if [ "$log_lines" -gt 15 ]; then log_lines=15; fi
149
219
 
150
220
  for i in $(seq 1 "$team_count"); do
151
- render_team_section "$i"
221
+ render_team_section "$i" "$log_lines"
152
222
  echo ""
153
223
  done
154
-
155
- # Lead/시스템 이벤트 (team이 아닌 엔트리)
156
- echo -e "${BOLD}SYSTEM${RESET}"
157
- if [ -f "$PROGRESS_LOG" ]; then
158
- grep -v 'team-[0-9]' "$PROGRESS_LOG" 2>/dev/null | grep -v '^#' | grep -v '^$' | tail -5 | while IFS= read -r line; do
159
- local ts agent action detail
160
- ts=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$1); print $1}')
161
- agent=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$2); print $2}')
162
- action=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
163
- detail=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
164
-
165
- local short_ts
166
- short_ts=$(echo "$ts" | grep -oE '[0-9]{2}:[0-9]{2}' | tail -1 || echo "$ts")
167
-
168
- if [ ${#detail} -gt 35 ]; then detail="${detail:0:33}.."; fi
169
-
170
- printf " %b%s%b %s %b%s%b\n" \
171
- "$DIM" "$short_ts" "$RESET" \
172
- "$agent" \
173
- "$DIM" "$detail" "$RESET"
174
- done
175
- fi
176
224
  }
177
225
 
178
226
  # ══════════════════════════════════════════
179
- # v3 모드: 단일 스트림 (기존 동작)
227
+ # Solo 모드: 단일 스트림 (기존 동작)
180
228
  # ══════════════════════════════════════════
181
229
 
182
230
  LAST_AGENT=""
@@ -285,26 +333,31 @@ cleanup() {
285
333
  }
286
334
  trap cleanup EXIT INT TERM
287
335
 
288
- # 매 루프마다 v4/v3 동적 감지 — 시작 후에도 모드 전환 가능
336
+ # 매 루프마다 mode 동적 감지 — 시작 후에도 모드 전환 가능
289
337
  tput civis 2>/dev/null
290
338
  clear
291
339
 
292
340
  CURRENT_MODE=""
293
341
 
294
342
  while true; do
295
- # 동적 모드 감지
296
- if [ -f "$QUEUE" ]; then
297
- NEW_MODE="v4"
343
+ # 동적 모드 감지 (--team 지정 시 항상 team, 그 외 progress.json.mode 참조)
344
+ if [ -n "$TEAM_FILTER" ] || [ -f "$QUEUE" ]; then
345
+ NEW_MODE="team"
298
346
  else
299
- NEW_MODE="v3"
347
+ if [ -f "$PROGRESS" ] && command -v jq &>/dev/null; then
348
+ NEW_MODE=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
349
+ [ "$NEW_MODE" = "team" ] || NEW_MODE="solo"
350
+ else
351
+ NEW_MODE="solo"
352
+ fi
300
353
  fi
301
354
 
302
355
  # 모드 전환 시 화면 초기화
303
356
  if [ "$NEW_MODE" != "$CURRENT_MODE" ]; then
304
357
  clear
305
358
  CURRENT_MODE="$NEW_MODE"
306
- if [ "$CURRENT_MODE" = "v3" ]; then
307
- # v3 초기화
359
+ if [ "$CURRENT_MODE" = "solo" ]; then
360
+ # solo 초기화
308
361
  if [ -f "$PROGRESS" ]; then
309
362
  LAST_AGENT=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
310
363
  LAST_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
@@ -314,7 +367,7 @@ while true; do
314
367
  fi
315
368
  fi
316
369
 
317
- if [ "$CURRENT_MODE" = "v4" ]; then
370
+ if [ "$CURRENT_MODE" = "team" ]; then
318
371
  buf=$(render_v4 2>&1)
319
372
  tput cup 0 0 2>/dev/null
320
373
  echo "$buf"
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # harness-prompt-history.sh — Prompt History pane for v4 tmux layout
2
+ # harness-prompt-history.sh — Prompt History pane for tmux layout
3
3
  # progress.log + audit.log에서 유저 프롬프트/에이전트 활동 히스토리를 표시
4
4
  # Usage: bash scripts/harness-prompt-history.sh [project-root]
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # harness-queue-manager.sh — Feature Queue Manager (v4.0)
2
+ # harness-queue-manager.sh — Feature Queue Manager (v5 unified)
3
3
  #
4
4
  # feature-list.json에서 depends_on 그래프를 읽어 topological sort 후
5
5
  # feature-queue.json을 생성/관리한다.
@@ -3,7 +3,7 @@
3
3
  # 새 세션 시작 시 자동으로:
4
4
  # 1) 이전 에이전트가 completed이면 harness-next.sh 실행 (게이트 + handoff)
5
5
  # 2) Planner/Dispatcher 사이클이면 audit 리셋
6
- # 3) 다음 에이전트 안내 출력
6
+ # 3) 모드별 안내 출력
7
7
 
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  LIB="$SCRIPT_DIR/lib/harness-render-progress.sh"
@@ -25,22 +25,33 @@ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
25
25
  current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
26
26
  next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
27
27
  agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
28
+ mode=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
28
29
 
29
30
  # ─────────────────────────────────────────
30
- # v4 Parallel Mode — feature-queue.json이 존재하면 v4 안내
31
+ # Team Mode — 팀이 자율 실행 중이면 오케스트레이터 안내
31
32
  # ─────────────────────────────────────────
32
- FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
33
- if [ -f "$FEATURE_QUEUE" ]; then
34
- passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
35
- total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
36
- in_prog=$(jq '.queue.in_progress | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
37
- failed=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
38
-
39
- echo "# Harness v4 — Parallel Agent Teams active"
33
+ if [ "$mode" = "team" ]; then
34
+ FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
35
+ passed=0; total=0; in_prog=0; failed=0
36
+ if [ -f "$FEATURE_QUEUE" ]; then
37
+ passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
38
+ total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
39
+ in_prog=$(jq '.queue.in_progress | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
40
+ failed=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
41
+ fi
42
+
43
+ echo "# Harness Team Mode active"
40
44
  echo "# Queue: ${passed}/${total} passed, ${in_prog} in progress, ${failed} failed"
41
- echo "# Teams are running autonomously. You are the orchestrator."
42
- echo "# Role: Monitor dashboard, resolve failures, manual interventions only."
43
- echo "# Do NOT run /harness-generator-* or /harness-evaluator-* — Teams handle Gen+Eval."
45
+ echo "# Teams run autonomously (Gen-Eval loop, max 5 retries)."
46
+ echo "# Stop: /harness-stop | Switch to solo: /harness-solo"
47
+ exit 0
48
+ fi
49
+
50
+ # ─────────────────────────────────────────
51
+ # Paused Mode — 중단 상태 안내
52
+ # ─────────────────────────────────────────
53
+ if [ "$mode" = "paused" ]; then
54
+ echo "# Harness paused — resume with /harness-team or /harness-solo"
44
55
  exit 0
45
56
  fi
46
57
 
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
- # harness-statusline.sh — Claude Code statusline hook
2
+ # harness-statusline.sh — Claude Code statusline hook (v5 unified)
3
3
  # 터미널 하단에 항상 고정되는 1줄 compact 상태 표시.
4
- # stdin: Claude Code JSON payload (model, context_window, cost 등)
5
- # stdout: 상태 문자열 (Claude Code가 터미널 하단에 렌더링)
4
+ # Solo: [S1] FULL | solo | >backend | sonnet | 2/5 feat | ctx 45% | $1.23
5
+ # Team: [S1] FULL | team | T1:gen T2:eval T3:idle | 2/5 feat | ctx 45% | $1.23
6
6
 
7
7
  # Read Claude Code session data from stdin
8
8
  input=$(cat)
@@ -39,6 +39,7 @@ current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
39
39
  agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
40
40
  next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
41
41
  retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS" 2>/dev/null)
42
+ harness_mode=$(jq -r '.mode // "solo"' "$PROGRESS" 2>/dev/null)
42
43
 
43
44
  # Pipeline short name
44
45
  case "$pipeline" in
@@ -106,10 +107,32 @@ if [ -f "$CONFIG" ] && [ "$active_agent" != "none" ] && [ "$active_agent" != "nu
106
107
  fi
107
108
  fi
108
109
 
109
- # Build compact status line
110
- # Format: [S1] FULL | >backend | sonnet | 2/5 feat | ctx 45% | $1.23
111
- if [ -n "$model_short" ]; then
112
- echo "[S${sprint_num}] ${pl} | ${status_icon}${agent_short}${retry_str} | ${model_short} | ${completed_features}/${total_features} feat | ctx ${context_pct}% | \$${cost}"
110
+ # Build compact status line based on mode
111
+ FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
112
+
113
+ if [ "$harness_mode" = "team" ] && [ -f "$FEATURE_QUEUE" ]; then
114
+ # Team mode: show team status instead of single agent
115
+ team_info=""
116
+ for i in 1 2 3; do
117
+ t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$FEATURE_QUEUE" 2>/dev/null)
118
+ t_feature=$(jq -r ".teams[\"$i\"].feature // \"\"" "$FEATURE_QUEUE" 2>/dev/null)
119
+ if [ "$t_status" = "busy" ] && [ -n "$t_feature" ] && [ "$t_feature" != "null" ]; then
120
+ t_phase=$(jq -r --arg f "$t_feature" '.queue.in_progress[$f].phase // "?"' "$FEATURE_QUEUE" 2>/dev/null)
121
+ team_info+="T${i}:${t_phase} "
122
+ else
123
+ team_info+="T${i}:- "
124
+ fi
125
+ done
126
+ t_passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
127
+ t_total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
128
+
129
+ echo "[S${sprint_num}] ${pl} | team | ${team_info}| ${t_passed}/${t_total} feat | ctx ${context_pct}% | \$${cost}"
113
130
  else
114
- echo "[S${sprint_num}] ${pl} | ${status_icon}${agent_short}${retry_str} | ${completed_features}/${total_features} feat | ctx ${context_pct}% | \$${cost}"
131
+ # Solo/paused mode: show single agent status
132
+ mode_tag="${harness_mode}"
133
+ if [ -n "$model_short" ]; then
134
+ echo "[S${sprint_num}] ${pl} | ${mode_tag} | ${status_icon}${agent_short}${retry_str} | ${model_short} | ${completed_features}/${total_features} feat | ctx ${context_pct}% | \$${cost}"
135
+ else
136
+ echo "[S${sprint_num}] ${pl} | ${mode_tag} | ${status_icon}${agent_short}${retry_str} | ${completed_features}/${total_features} feat | ctx ${context_pct}% | \$${cost}"
137
+ fi
115
138
  fi