@walwal-harness/cli 6.0.5 → 6.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/commands/harness-solo.md +20 -20
  3. package/commands/harness-team.md +1 -1
  4. package/gotchas/coo-developer.md +13 -0
  5. package/gotchas/documentationer.md +13 -0
  6. package/package.json +1 -1
  7. package/scripts/conductor-tick.sh +416 -0
  8. package/scripts/harness-archive.sh +123 -0
  9. package/scripts/harness-dashboard-up.sh +72 -0
  10. package/scripts/harness-dashboard.sh +509 -0
  11. package/scripts/harness-goal-init.sh +72 -0
  12. package/scripts/harness-goal-show.sh +37 -0
  13. package/scripts/harness-gotcha-memory.sh +348 -0
  14. package/scripts/harness-gotcha-register.sh +393 -0
  15. package/scripts/harness-meeting-doc.sh +153 -0
  16. package/scripts/harness-monitor.sh +398 -0
  17. package/scripts/harness-next.sh +577 -0
  18. package/scripts/harness-progress-set.sh +41 -0
  19. package/scripts/harness-prompt-history.sh +164 -0
  20. package/scripts/harness-queue-manager.sh +694 -0
  21. package/scripts/harness-session-start.sh +140 -0
  22. package/scripts/harness-statusline.sh +139 -0
  23. package/scripts/harness-task-session.sh +48 -0
  24. package/scripts/harness-tmux.sh +372 -0
  25. package/scripts/harness-user-prompt-submit.sh +123 -0
  26. package/scripts/init-agents-md.sh +337 -0
  27. package/scripts/init-ref-docs.sh +262 -0
  28. package/scripts/lib/harness-audit.sh +113 -0
  29. package/scripts/lib/harness-guardrail.sh +148 -0
  30. package/scripts/lib/harness-keywait.sh +57 -0
  31. package/scripts/lib/harness-render-progress.sh +346 -0
  32. package/scripts/scan-project.sh +367 -0
  33. package/skills/brainstorming/SKILL.md +4 -4
  34. package/skills/conductor/SKILL.md +19 -16
  35. package/skills/coo-developer/SKILL.md +42 -0
  36. package/skills/cqo/SKILL.md +7 -0
  37. package/skills/cto/SKILL.md +7 -0
  38. package/skills/dispatcher/SKILL.md +17 -28
  39. package/skills/dispatcher/persona-ceo.md +2 -1
  40. package/skills/documentationer/SKILL.md +46 -0
  41. package/skills/evaluator-code-quality/SKILL.md +1 -1
  42. package/skills/evaluator-functional/SKILL.md +1 -1
  43. package/skills/evaluator-visual/SKILL.md +1 -1
  44. package/skills/generator-backend/SKILL.md +1 -1
  45. package/skills/generator-frontend/SKILL.md +1 -1
  46. package/skills/meeting-manager/SKILL.md +34 -1
  47. package/skills/planner/SKILL.md +23 -9
  48. package/skills/planner/persona-coo-hr.md +17 -0
  49. package/skills/service-ops/SKILL.md +19 -4
@@ -0,0 +1,577 @@
1
+ #!/bin/bash
2
+ # harness-next.sh — 세션 오케스트레이터
3
+ # 현재 progress.json 상태를 읽고 다음 에이전트를 결정한다.
4
+ # Feature-level 프로그래스를 출력하고 handoff.json을 생성한다.
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
9
+ source "$SCRIPT_DIR/lib/harness-guardrail.sh"
10
+ source "$SCRIPT_DIR/lib/harness-audit.sh"
11
+
12
+ # ─────────────────────────────────────────
13
+ # Resolve project root
14
+ # ─────────────────────────────────────────
15
+ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || {
16
+ echo "[harness] ERROR: .harness/ directory not found" >&2
17
+ echo " Run 'npx walwal-harness' to initialize." >&2
18
+ exit 1
19
+ }
20
+
21
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
22
+ CONFIG="$PROJECT_ROOT/.harness/config.json"
23
+ PIPELINE_JSON="$PROJECT_ROOT/.harness/actions/pipeline.json"
24
+ HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
25
+
26
+ check_jq || exit 1
27
+
28
+ # Initialize audit log
29
+ init_audit "$PROJECT_ROOT"
30
+
31
+ if [ ! -f "$PROGRESS" ]; then
32
+ echo "[harness] ERROR: progress.json not found" >&2
33
+ echo " Run 'npx walwal-harness --force' to reinitialize." >&2
34
+ exit 1
35
+ fi
36
+
37
+ # ─────────────────────────────────────────
38
+ # Read current state
39
+ # ─────────────────────────────────────────
40
+ pipeline=$(jq -r '.pipeline // "null"' "$PROGRESS")
41
+ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS")
42
+ sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS")
43
+ current_agent=$(jq -r '.current_agent // "null"' "$PROGRESS")
44
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
45
+ next_agent=$(jq -r '.next_agent // "null"' "$PROGRESS")
46
+ retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS")
47
+ max_retries=$(jq -r '.flow.max_retries_per_sprint // 10' "$CONFIG" 2>/dev/null || echo 10)
48
+
49
+ # fe_stack + fe_target 치환 (Flutter Web/Mobile/Desktop 지원) — pipeline.json 에서 읽음
50
+ fe_stack="react"
51
+ fe_target="web"
52
+ if [ -f "$PIPELINE_JSON" ]; then
53
+ fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
54
+ fe_target=$(jq -r '.fe_target // empty' "$PIPELINE_JSON" 2>/dev/null || true)
55
+ if [ -z "$fe_target" ]; then
56
+ # pipeline.json 에 fe_target 미지정 시 config.json 의 _default_target 사용
57
+ fe_target="web" # v5.6.5+: 치환 로직 제거. fe_target 은 pipeline.json 에서 명시하거나 web 기본.
58
+ fi
59
+ fi
60
+
61
+ # ─────────────────────────────────────────
62
+ # fe_stack 치환 (no-op — Flutter 지원 제거됨, 하위 호환용 stub)
63
+ # ─────────────────────────────────────────
64
+ substitute_fe_stack() {
65
+ echo "$1"
66
+ }
67
+
68
+ # ─────────────────────────────────────────
69
+ # Pre-Eval Gate — deterministic checks before Evaluator
70
+ # Generator가 완료되고 다음이 Evaluator일 때, lint/type/test를 먼저 실행.
71
+ # 실패 시 Evaluator를 건너뛰고 Generator로 리라우팅.
72
+ # ─────────────────────────────────────────
73
+ run_pre_eval_gate() {
74
+ local next="$1"
75
+ local gate_enabled
76
+ gate_enabled=$(jq -r '.flow.pre_eval_gate.enabled // false' "$CONFIG" 2>/dev/null)
77
+ if [ "$gate_enabled" != "true" ]; then return 0; fi
78
+
79
+ # Evaluator 에이전트인지 확인
80
+ case "$next" in
81
+ evaluator-*) ;;
82
+ *) return 0 ;;
83
+ esac
84
+
85
+ local timeout
86
+ timeout=$(jq -r '.flow.pre_eval_gate.timeout_seconds // 120' "$CONFIG" 2>/dev/null)
87
+
88
+ # 실패 위치 결정 (backend or frontend)
89
+ # current_agent가 실패로 변경된 경우, completed_agents에서 직전 Generator를 찾는다
90
+ local location="backend"
91
+ local checks_key="backend_checks"
92
+ local source_agent="$current_agent"
93
+
94
+ # current_agent가 generator가 아닌 경우 (예: dispatcher로 리라우팅된 상태),
95
+ # completed_agents에서 마지막 generator를 찾는다
96
+ case "$source_agent" in
97
+ generator-frontend)
98
+ location="frontend"
99
+ checks_key="frontend_checks"
100
+ ;;
101
+ generator-backend)
102
+ location="backend"
103
+ checks_key="backend_checks"
104
+ ;;
105
+ *)
106
+ # completed_agents에서 마지막 generator를 역순으로 찾기
107
+ local last_gen
108
+ last_gen=$(jq -r '.completed_agents // [] | map(select(startswith("generator-"))) | last // empty' "$PROGRESS" 2>/dev/null)
109
+ case "$last_gen" in
110
+ generator-frontend)
111
+ location="frontend"
112
+ checks_key="frontend_checks"
113
+ ;;
114
+ generator-backend)
115
+ location="backend"
116
+ checks_key="backend_checks"
117
+ ;;
118
+ esac
119
+ ;;
120
+ esac
121
+
122
+ local -a checks
123
+ mapfile -t checks < <(jq -r ".flow.pre_eval_gate.${checks_key}[]" "$CONFIG" 2>/dev/null)
124
+
125
+ if [ ${#checks[@]} -eq 0 ]; then return 0; fi
126
+
127
+ # Resolve cwd for checks: config.pre_eval_gate.frontend_cwd / backend_cwd
128
+ local cwd_key="${location}_cwd"
129
+ local check_cwd
130
+ check_cwd=$(jq -r ".flow.pre_eval_gate.${cwd_key} // empty" "$CONFIG" 2>/dev/null)
131
+ if [ -n "$check_cwd" ] && [ "$check_cwd" != "null" ]; then
132
+ # Relative path from PROJECT_ROOT
133
+ check_cwd="$PROJECT_ROOT/$check_cwd"
134
+ else
135
+ check_cwd="$PROJECT_ROOT"
136
+ fi
137
+
138
+ echo ""
139
+ echo " ── Pre-Eval Gate ──────────────────────"
140
+ if [ "$check_cwd" != "$PROJECT_ROOT" ]; then
141
+ echo " cwd: ${check_cwd#$PROJECT_ROOT/}"
142
+ fi
143
+ local all_pass=true
144
+ local fail_log=""
145
+
146
+ for cmd in "${checks[@]}"; do
147
+ printf " %-40s " "$cmd"
148
+ local output
149
+ if output=$(cd "$check_cwd" && timeout "${timeout}s" bash -c "$cmd" 2>&1); then
150
+ echo "✓"
151
+ else
152
+ echo "✗"
153
+ all_pass=false
154
+ fail_log+="[FAIL] $cmd"$'\n'"$output"$'\n\n'
155
+ fi
156
+ done
157
+
158
+ if [ "$all_pass" = true ]; then
159
+ echo " Gate: PASS — proceeding to $next"
160
+ echo ""
161
+ return 0
162
+ else
163
+ echo ""
164
+ echo " Gate: FAIL — rerouting to $current_agent"
165
+ echo ""
166
+
167
+ # progress.json 업데이트: 실패 기록 + Generator로 리라우팅
168
+ local new_retry=$((retry_count + 1))
169
+ local fail_summary
170
+ fail_summary=$(echo "$fail_log" | head -20)
171
+
172
+ jq --arg agent "$current_agent" \
173
+ --arg loc "$location" \
174
+ --arg msg "Pre-eval gate failed: $fail_summary" \
175
+ --arg target "$current_agent" \
176
+ --argjson retry "$new_retry" \
177
+ '.sprint.status = "failed" |
178
+ .sprint.retry_count = $retry |
179
+ .agent_status = "failed" |
180
+ .next_agent = $target |
181
+ .failure.agent = $agent |
182
+ .failure.location = $loc |
183
+ .failure.message = $msg |
184
+ .failure.retry_target = $target' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
185
+
186
+ # next_agent를 Generator로 덮어쓰기
187
+ next_agent="$current_agent"
188
+ return 1
189
+ fi
190
+ }
191
+
192
+ # ─────────────────────────────────────────
193
+ # Conductor Tick — lightweight 6.0 routing bridge
194
+ # next_agent 계산 후, Company loop가 개입해야 하면 next_agent를 재작성.
195
+ # ─────────────────────────────────────────
196
+ run_conductor_tick() {
197
+ local candidate="$1"
198
+ local owner
199
+ owner=$(jq -r '.mode_selection.owner // empty' "$CONFIG" 2>/dev/null || true)
200
+ if [ "$owner" != "conductor" ]; then return 0; fi
201
+ if [ ! -x "$SCRIPT_DIR/conductor-tick.sh" ]; then return 0; fi
202
+
203
+ HARNESS_CONDUCTOR_CANDIDATE="$candidate" bash "$SCRIPT_DIR/conductor-tick.sh" "$PROJECT_ROOT" >/dev/null 2>&1 || true
204
+ next_agent=$(jq -r '.next_agent // "null"' "$PROGRESS" 2>/dev/null || echo "$candidate")
205
+ }
206
+
207
+ # ─────────────────────────────────────────
208
+ # Determine next agent
209
+ # ─────────────────────────────────────────
210
+ compute_next_agent() {
211
+ local current="$1"
212
+ local status="$2"
213
+
214
+ # If blocked, no next
215
+ if [ "$status" = "blocked" ]; then
216
+ echo "null"
217
+ return
218
+ fi
219
+
220
+ # If failure with retry target, use that
221
+ local retry_target
222
+ retry_target=$(jq -r '.failure.retry_target // "null"' "$PROGRESS")
223
+ if [ "$retry_target" != "null" ] && [ "$status" = "failed" ]; then
224
+ echo "$retry_target"
225
+ return
226
+ fi
227
+
228
+ # If next_agent is already set in progress.json, use it
229
+ if [ "$next_agent" != "null" ]; then
230
+ echo "$next_agent"
231
+ return
232
+ fi
233
+
234
+ # Compute from pipeline sequence
235
+ if [ "$pipeline" = "null" ] || [ ! -f "$CONFIG" ]; then
236
+ echo "dispatcher"
237
+ return
238
+ fi
239
+
240
+ local -a raw_agents
241
+ mapfile -t raw_agents < <(jq -r ".flow.pipeline_selection.pipelines[\"${pipeline}\"][]" "$CONFIG" 2>/dev/null | sed 's/:.*//')
242
+
243
+ # fe_stack 치환 + __skip__ 필터링
244
+ local -a agents
245
+ for a in "${raw_agents[@]}"; do
246
+ local sub
247
+ sub=$(substitute_fe_stack "$a")
248
+ if [ "$sub" != "__skip__" ]; then
249
+ agents+=("$sub")
250
+ fi
251
+ done
252
+
253
+ local found=false
254
+ for agent in "${agents[@]}"; do
255
+ if [ "$found" = true ]; then
256
+ echo "$agent"
257
+ return
258
+ fi
259
+ # current 비교 시에도 치환된 이름으로 (Flutter 변형 에이전트가 실행되는 경우)
260
+ if [ "$agent" = "$current" ]; then
261
+ found=true
262
+ fi
263
+ done
264
+
265
+ # Current is last agent → sprint complete
266
+ echo "archive"
267
+ }
268
+
269
+ # If current agent completed, compute next
270
+ if [ "$agent_status" = "completed" ] && [ "$next_agent" = "null" ]; then
271
+ next_agent=$(compute_next_agent "$current_agent" "$agent_status")
272
+ fi
273
+
274
+ # ─────────────────────────────────────────
275
+ # Artifact Prerequisites — 선행 아티팩트 상태 검증
276
+ # ─────────────────────────────────────────
277
+ verify_artifact_prerequisites() {
278
+ local target_agent="$1"
279
+ local states_order='["pending","draft","reviewed","approved"]'
280
+
281
+ # 에이전트의 prerequisites 가져오기
282
+ local prereqs
283
+ prereqs=$(jq -r ".artifacts.prerequisites[\"${target_agent}\"] // empty" "$CONFIG" 2>/dev/null)
284
+ if [ -z "$prereqs" ] || [ "$prereqs" = "null" ]; then return 0; fi
285
+
286
+ local all_met=true
287
+ echo ""
288
+ echo " ── Artifact Prerequisites ─────────────"
289
+
290
+ # prereqs의 각 키(artifact명)를 순회
291
+ while IFS='=' read -r artifact required_status; do
292
+ artifact=$(echo "$artifact" | tr -d '"' | tr -d ' ')
293
+ required_status=$(echo "$required_status" | tr -d '"' | tr -d ' ')
294
+ if [ -z "$artifact" ]; then continue; fi
295
+
296
+ local current_status
297
+ current_status=$(jq -r ".artifacts[\"${artifact}\"].status // \"pending\"" "$PROGRESS" 2>/dev/null)
298
+
299
+ # 상태 순서 비교
300
+ local required_idx current_idx
301
+ required_idx=$(echo "$states_order" | jq "index(\"$required_status\") // 0")
302
+ current_idx=$(echo "$states_order" | jq "index(\"$current_status\") // 0")
303
+
304
+ if [ "$current_idx" -ge "$required_idx" ]; then
305
+ printf " ✓ %-25s %s (required: %s)\n" "$artifact" "$current_status" "$required_status"
306
+ else
307
+ printf " ✗ %-25s %s (required: %s)\n" "$artifact" "$current_status" "$required_status"
308
+ all_met=false
309
+ fi
310
+ done < <(jq -r ".artifacts.prerequisites[\"${target_agent}\"] | to_entries[] | \"\(.key)=\(.value)\"" "$CONFIG" 2>/dev/null)
311
+
312
+ echo ""
313
+
314
+ if [ "$all_met" = true ]; then
315
+ echo " Prerequisites: PASS"
316
+ else
317
+ echo " Prerequisites: FAIL — preceding agent must complete artifacts first"
318
+ fi
319
+ echo ""
320
+
321
+ [ "$all_met" = true ] && return 0 || return 1
322
+ }
323
+
324
+ # ─────────────────────────────────────────
325
+ # Runtime Guardrail — 파일 소유권 검증
326
+ # ─────────────────────────────────────────
327
+ if [ "$agent_status" = "completed" ]; then
328
+ audit_log "$current_agent" "develop" "complete" "" "agent completed"
329
+ verify_file_ownership "$PROJECT_ROOT" || audit_gate "file-ownership" "warn" "boundary violation detected"
330
+ fi
331
+
332
+ # ─────────────────────────────────────────
333
+ # Auto Gotcha Registration — Evaluator 완료 직후 evaluation-*.md 스캔
334
+ # evaluation-*.md 내부의 ```gotcha_candidates``` JSON 블록을 읽어
335
+ # .harness/gotchas/<target>.md 에 unverified 상태로 등록/dedup.
336
+ # ─────────────────────────────────────────
337
+ case "$current_agent" in
338
+ evaluator-*|generator-*)
339
+ if [ "$agent_status" = "completed" ] || [ "$agent_status" = "failed" ]; then
340
+ if [ -x "$SCRIPT_DIR/harness-gotcha-register.sh" ]; then
341
+ # --scan-all 로 evaluation-*.md + gen-report-*.md + lead-report-*.md 의
342
+ # gotcha_candidates / convention_candidates 블록 모두 동적 등록
343
+ bash "$SCRIPT_DIR/harness-gotcha-register.sh" "$PROJECT_ROOT" --scan-all 2>&1 \
344
+ | grep -E '^\[gotcha-register\]' || true
345
+ audit_gate "gotcha-register" "scan-all" "$current_agent"
346
+ fi
347
+ fi
348
+ ;;
349
+ esac
350
+
351
+ # ─────────────────────────────────────────
352
+ # Run Pre-Eval Gate (if applicable)
353
+ # ─────────────────────────────────────────
354
+ if [ "$agent_status" = "completed" ]; then
355
+ audit_gate "pre-eval" "start" "$next_agent"
356
+ if run_pre_eval_gate "$next_agent"; then
357
+ audit_gate "pre-eval" "pass" "$next_agent"
358
+ else
359
+ audit_gate "pre-eval" "fail" "rerouted to $current_agent"
360
+ fi
361
+ if verify_artifact_prerequisites "$next_agent"; then
362
+ audit_gate "artifact-prereq" "pass" "$next_agent"
363
+ else
364
+ audit_gate "artifact-prereq" "fail" "$next_agent"
365
+ fi
366
+ fi
367
+
368
+ # ─────────────────────────────────────────
369
+ # Company routing bridge — handoff 직전 conductor 가 next_agent 조정
370
+ # ─────────────────────────────────────────
371
+ if [ "$next_agent" != "null" ] && [ "$agent_status" != "blocked" ]; then
372
+ run_conductor_tick "$next_agent"
373
+ fi
374
+
375
+ # ─────────────────────────────────────────
376
+ # Render progress
377
+ # ─────────────────────────────────────────
378
+ render_progress "$PROJECT_ROOT"
379
+ render_agent_bar "$PROJECT_ROOT"
380
+ echo ""
381
+
382
+ # ─────────────────────────────────────────
383
+ # Generate handoff.json (unified session transition document)
384
+ # ─────────────────────────────────────────
385
+ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_status" != "blocked" ]; then
386
+ # ── Escalation check: 3회 실패 시 Planner에게 scope 축소 요청 ──
387
+ escalate_after=$(jq -r '.flow.escalate_to_planner_after // 3' "$CONFIG" 2>/dev/null || echo 3)
388
+ if [ "$retry_count" -ge "$escalate_after" ] && [ "$sprint_status" = "failed" ] && [ "$next_agent" != "planner" ]; then
389
+ echo " ⚠ Escalation: ${retry_count}회 실패 — Planner에게 scope 축소/접근 변경 요청"
390
+ audit_log "system" "escalate" "start" "→planner" "retry ${retry_count}회 초과"
391
+ next_agent="planner"
392
+ jq --arg msg "Escalated after ${retry_count} failures. Planner must review scope or approach." \
393
+ '.next_agent = "planner" |
394
+ .failure.message = $msg |
395
+ .failure.retry_target = "planner"' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
396
+ fi
397
+
398
+ # ── Read model & thinking mode for next agent ──
399
+ agent_model=$(jq -r ".agents[\"${next_agent}\"].model // \"opus\"" "$CONFIG" 2>/dev/null || echo "opus")
400
+ agent_thinking=$(jq -r ".agents[\"${next_agent}\"].thinking_mode // \"null\"" "$CONFIG" 2>/dev/null || echo "null")
401
+ task_session_path=""
402
+ if [ -x "$SCRIPT_DIR/harness-task-session.sh" ]; then
403
+ task_session_path=$(bash "$SCRIPT_DIR/harness-task-session.sh" "$PROJECT_ROOT" "$next_agent" "${current_agent:-dispatcher}" 2>/dev/null || true)
404
+ fi
405
+
406
+ # ── Build prompt text (embedded in handoff.json) ──
407
+ prompt="/harness-${next_agent} 를 실행하세요."
408
+
409
+ if [ "$sprint_num" -gt 0 ]; then
410
+ prompt+=$'\n'"Sprint ${sprint_num}"
411
+ if [ "$sprint_status" = "failed" ]; then
412
+ prompt+=" (retry ${retry_count}/${max_retries})"
413
+ fi
414
+ prompt+="을 진행합니다."
415
+ fi
416
+
417
+ prompt+=$'\n'".harness/handoff.json을 읽고 컨텍스트를 확인하세요."
418
+ if [ -n "$task_session_path" ]; then
419
+ prompt+=$'\n'"${task_session_path} 를 현재 task session의 단일 기준 문서로 사용하세요."
420
+ fi
421
+
422
+ # Inject thinking mode instruction
423
+ if [ "$agent_thinking" != "null" ]; then
424
+ case "$agent_thinking" in
425
+ ultraplan)
426
+ prompt+=$'\n\n'"[Thinking Mode: ultraplan] /${agent_thinking} 모드를 사용하세요. 깊은 사고로 아키텍처와 설계를 수행합니다."
427
+ ;;
428
+ ultrathink)
429
+ prompt+=$'\n\n'"[Thinking Mode: ultrathink] /${agent_thinking} 모드를 사용하세요. 최대 추론 깊이로 비판적 검증을 수행합니다."
430
+ ;;
431
+ plan)
432
+ prompt+=$'\n\n'"[Thinking Mode: plan] /${agent_thinking} 모드를 사용하세요. 구조화된 계획을 수립한 후 실행합니다."
433
+ ;;
434
+ esac
435
+ fi
436
+
437
+ # Add failure context if retrying
438
+ failure_msg=$(jq -r '.failure.message // empty' "$PROGRESS")
439
+ if [ -n "$failure_msg" ] && [ "$failure_msg" != "null" ]; then
440
+ prompt+=$'\n\n'"이전 실패 사유: ${failure_msg}"
441
+ prompt+=$'\n'"같은 접근을 반복하지 말고, 실패 원인을 분석한 후 다른 전략으로 시도하세요."
442
+ fi
443
+
444
+ # ── Collect artifacts ──
445
+ FEATURE_LIST="$PROJECT_ROOT/.harness/actions/feature-list.json"
446
+
447
+ artifacts_ready=()
448
+ for f in plan.md feature-list.json api-contract.json sprint-contract.md evaluation-functional.md evaluation-visual.md; do
449
+ if [ -f "$PROJECT_ROOT/.harness/actions/$f" ]; then
450
+ artifacts_ready+=("$f")
451
+ fi
452
+ done
453
+ artifacts_json=$(printf '%s\n' "${artifacts_ready[@]}" | jq -R . | jq -s .)
454
+
455
+ # Collect focus features (incomplete ones)
456
+ focus_features="[]"
457
+ if [ -f "$FEATURE_LIST" ]; then
458
+ focus_features=$(jq '[.features[]? | select(.passes == null or (.passes | length) == 0 or ((.passes // []) | map(select(. == "evaluator-functional")) | length == 0)) | .id] | .[0:5]' "$FEATURE_LIST" 2>/dev/null || echo "[]")
459
+ fi
460
+
461
+ # ── Regression data ──
462
+ regression_source="null"
463
+ prev_sprint=$((sprint_num - 1))
464
+ prev_archive="$PROJECT_ROOT/.harness/archive/sprint-$(printf '%03d' $prev_sprint)"
465
+ if [ "$prev_sprint" -ge 1 ] && [ -d "$prev_archive" ]; then
466
+ if [ -f "$prev_archive/feature-list.json" ]; then
467
+ regression_source=$(jq '{
468
+ sprint: '"$prev_sprint"',
469
+ passed_features: [.features[]? | select((.passes // []) | map(select(. == "evaluator-functional")) | length > 0) | {id, name, acceptance_criteria}],
470
+ archive_path: "'"$prev_archive"'"
471
+ }' "$prev_archive/feature-list.json" 2>/dev/null || echo "null")
472
+ fi
473
+ fi
474
+
475
+ # ── Eval-specific config ──
476
+ eval_config="null"
477
+ cross_validation_data="null"
478
+ case "$next_agent" in
479
+ evaluator-*)
480
+ eval_config=$(jq '{
481
+ pass_threshold: .evaluation.scoring.pass_threshold,
482
+ scale: .evaluation.scoring.scale,
483
+ verdict_rules: .evaluation.scoring.verdict_rules,
484
+ regression_enabled: .evaluation.regression.enabled,
485
+ cross_validation_enabled: .evaluation.cross_validation.enabled,
486
+ adversarial_rules: .agents["'"$next_agent"'"].adversarial_rules.rules,
487
+ forbidden: .agents["'"$next_agent"'"].adversarial_rules.forbidden
488
+ }' "$CONFIG" 2>/dev/null || echo "null")
489
+ ;;
490
+ esac
491
+
492
+ # ── Cross-Validation (chain: code-quality → functional → visual) ──
493
+ cross_validation_from_code_quality="null"
494
+ if [ "$next_agent" = "evaluator-functional" ] || [ "$next_agent" = "evaluator-visual" ]; then
495
+ cq_eval="$PROJECT_ROOT/.harness/actions/evaluation-code-quality.md"
496
+ if [ -f "$cq_eval" ]; then
497
+ cross_validation_from_code_quality=$(sed -n '/```json/,/```/p' "$cq_eval" | tail -n +2 | head -n -1 | jq 'select(.evaluator == "code-quality" or .cross_validation_from_code_quality)' 2>/dev/null || echo "null")
498
+ fi
499
+ fi
500
+ if [ "$next_agent" = "evaluator-visual" ]; then
501
+ func_eval="$PROJECT_ROOT/.harness/actions/evaluation-functional.md"
502
+ if [ -f "$func_eval" ]; then
503
+ cross_validation_data=$(sed -n '/```json/,/```/p' "$func_eval" | tail -n +2 | head -n -1 | jq 'select(.evaluator == "functional")' 2>/dev/null || echo "null")
504
+ fi
505
+ fi
506
+
507
+ # ── Build handoff.json (single source of truth for session transition) ──
508
+ jq -n \
509
+ --arg from "${current_agent:-dispatcher}" \
510
+ --arg to "$next_agent" \
511
+ --arg prompt "$prompt" \
512
+ --argjson sprint "$sprint_num" \
513
+ --argjson retry "$retry_count" \
514
+ --arg status "$sprint_status" \
515
+ --arg agent_model "$agent_model" \
516
+ --arg agent_thinking "$agent_thinking" \
517
+ --arg failure_msg "${failure_msg:-}" \
518
+ --argjson artifacts "$artifacts_json" \
519
+ --argjson focus "$focus_features" \
520
+ --argjson regression "$regression_source" \
521
+ --argjson eval_config "$eval_config" \
522
+ --argjson cross_val "$cross_validation_data" \
523
+ --argjson cross_val_cq "$cross_validation_from_code_quality" \
524
+ --arg task_session_path "$task_session_path" \
525
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
526
+ '{
527
+ from: $from,
528
+ to: $to,
529
+ prompt: $prompt,
530
+ sprint: $sprint,
531
+ retry_count: $retry,
532
+ sprint_status: $status,
533
+ model: $agent_model,
534
+ thinking_mode: (if $agent_thinking == "null" then null else $agent_thinking end),
535
+ failure_context: (if $failure_msg != "" then $failure_msg else null end),
536
+ artifacts_ready: $artifacts,
537
+ focus_features: $focus,
538
+ regression: $regression,
539
+ eval_config: $eval_config,
540
+ cross_validation_from_functional: $cross_val,
541
+ cross_validation_from_code_quality: $cross_val_cq,
542
+ task_session_path: (if $task_session_path == "" then null else $task_session_path end),
543
+ warnings: [],
544
+ timestamp: $timestamp
545
+ }' > "$HANDOFF"
546
+
547
+ audit_handoff "${current_agent:-dispatcher}" "$next_agent" "complete"
548
+
549
+ elif [ "$next_agent" = "archive" ]; then
550
+ audit_log "system" "archive" "start" "sprint-${sprint_num}" "sprint cycle complete"
551
+
552
+ # ── Auto-archive: run archive script synchronously ──
553
+ # harness-archive.sh moves .harness/actions/* to .harness/archive/D-NNN/S-NNN/
554
+ # and resets progress.json. On next user prompt the flow starts fresh as a new dispatch.
555
+ if bash "$SCRIPT_DIR/harness-archive.sh" "$PROJECT_ROOT"; then
556
+ audit_log "system" "archive" "complete" "sprint-${sprint_num}" "auto-archived"
557
+ else
558
+ echo " ⚠ archive script failed — falling back to manual handoff" >&2
559
+ audit_log "system" "archive" "fail" "sprint-${sprint_num}" "script failed"
560
+ jq -n \
561
+ --arg from "${current_agent:-evaluator}" \
562
+ --argjson sprint "$sprint_num" \
563
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
564
+ '{
565
+ from: $from,
566
+ to: "archive",
567
+ prompt: "Sprint 문서를 아카이브하세요. harness-archive.sh 가 실패했습니다 — 수동으로 .harness/actions/* 를 .harness/archive/D-NNN/S-NNN/ 로 이동하세요.",
568
+ sprint: $sprint,
569
+ model: "opus",
570
+ thinking_mode: null,
571
+ timestamp: $timestamp
572
+ }' > "$HANDOFF"
573
+ fi
574
+
575
+ else
576
+ echo '{}' > "$HANDOFF"
577
+ fi
@@ -0,0 +1,41 @@
1
+ #!/bin/bash
2
+ # harness-progress-set.sh — Safe partial update to .harness/progress.json
3
+ #
4
+ # Usage:
5
+ # bash scripts/harness-progress-set.sh <project-root> <jq_filter>
6
+ #
7
+ # The jq filter is a partial assignment expression. All other fields are
8
+ # preserved. NEVER rewrite progress.json as a full JSON blob — mode,
9
+ # team_state, and similar top-level fields will be lost.
10
+ #
11
+ # Example:
12
+ # bash scripts/harness-progress-set.sh . '.current_agent = "planner" | .agent_status = "running"'
13
+
14
+ set -uo pipefail
15
+
16
+ PROJECT_ROOT="${1:-.}"
17
+ FILTER="${2:-}"
18
+
19
+ if [ -z "$FILTER" ]; then
20
+ echo "[progress-set] usage: $0 <project-root> <jq_filter>" >&2
21
+ exit 2
22
+ fi
23
+
24
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
+
26
+ if [ ! -f "$PROGRESS" ]; then
27
+ echo "[progress-set] not found: $PROGRESS" >&2
28
+ exit 1
29
+ fi
30
+
31
+ # Always add .updated_at = now so callers don't forget
32
+ FINAL_FILTER="$FILTER | .updated_at = (now | todate)"
33
+
34
+ TMP="${PROGRESS}.tmp.$$"
35
+ if jq "$FINAL_FILTER" "$PROGRESS" > "$TMP" 2>/dev/null; then
36
+ mv "$TMP" "$PROGRESS"
37
+ else
38
+ rm -f "$TMP"
39
+ echo "[progress-set] jq filter failed: $FILTER" >&2
40
+ exit 1
41
+ fi