@walwal-harness/cli 6.1.3 → 6.1.5

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 (74) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +43 -74
  3. package/assets/launchd/com.walwal.harness-wake.plist.template +32 -0
  4. package/assets/templates/CONVENTIONS.md +12 -0
  5. package/assets/templates/HARNESS.md +216 -403
  6. package/assets/templates/config.json +27 -29
  7. package/assets/templates/memory.md +16 -2
  8. package/assets/templates/progress.json.template +6 -5
  9. package/bin/init.js +179 -90
  10. package/conventions/README.md +92 -0
  11. package/conventions/conductor.md +24 -0
  12. package/conventions/coo-developer.md +24 -0
  13. package/conventions/cqo.md +24 -0
  14. package/conventions/cto.md +24 -0
  15. package/conventions/dispatcher.md +24 -0
  16. package/conventions/documentationer.md +24 -0
  17. package/conventions/evaluator-architecture.md +24 -0
  18. package/conventions/evaluator-code-quality.md +24 -0
  19. package/conventions/evaluator-functional.md +24 -0
  20. package/conventions/evaluator-security.md +24 -0
  21. package/conventions/evaluator-visual.md +24 -0
  22. package/conventions/generator-backend.md +24 -0
  23. package/conventions/generator-designer.md +24 -0
  24. package/conventions/generator-devops.md +24 -0
  25. package/conventions/generator-frontend.md +24 -0
  26. package/conventions/meeting-manager.md +24 -0
  27. package/conventions/planner.md +24 -0
  28. package/conventions/service-ops.md +24 -0
  29. package/conventions/shared.md +40 -0
  30. package/gotchas/conductor.md +16 -0
  31. package/gotchas/dispatcher.md +16 -0
  32. package/gotchas/meeting-manager.md +8 -0
  33. package/gotchas/service-ops.md +8 -0
  34. package/package.json +5 -4
  35. package/scripts/conductor-tick.sh +61 -53
  36. package/scripts/harness-archive.sh +7 -5
  37. package/scripts/harness-dashboard-up.sh +11 -3
  38. package/scripts/harness-hourly-review.sh +303 -0
  39. package/scripts/harness-meeting-doc.sh +55 -16
  40. package/scripts/harness-next.sh +1 -1
  41. package/scripts/harness-queue-manager.sh +12 -5
  42. package/scripts/harness-service-ops-monitor.sh +228 -0
  43. package/scripts/harness-session-start.sh +60 -31
  44. package/scripts/harness-statusline.sh +8 -12
  45. package/scripts/harness-stop.sh +85 -0
  46. package/scripts/harness-user-prompt-submit.sh +11 -29
  47. package/scripts/harness-wake-install.sh +179 -0
  48. package/scripts/harness-wake.sh +258 -0
  49. package/scripts/harness-worker-dispatch.sh +157 -0
  50. package/scripts/lib/harness-progress-migrate.sh +6 -1
  51. package/scripts/lib/harness-render-progress.sh +3 -3
  52. package/skills/brainstorming/SKILL.md +2 -2
  53. package/skills/conductor/SKILL.md +67 -55
  54. package/skills/cqo/SKILL.md +1 -0
  55. package/skills/cto/SKILL.md +1 -0
  56. package/skills/dispatcher/SKILL.md +33 -14
  57. package/skills/evaluator-code-quality/SKILL.md +4 -4
  58. package/skills/evaluator-functional/SKILL.md +6 -6
  59. package/skills/evaluator-visual/SKILL.md +4 -4
  60. package/skills/generator-backend/SKILL.md +4 -4
  61. package/skills/generator-frontend/SKILL.md +5 -5
  62. package/skills/meeting-manager/SKILL.md +82 -12
  63. package/skills/planner/SKILL.md +3 -3
  64. package/skills/service-ops/SKILL.md +25 -0
  65. package/commands/harness-solo.md +0 -103
  66. package/commands/harness-stop.md +0 -53
  67. package/commands/harness-team.md +0 -530
  68. package/scripts/harness-dashboard.sh +0 -509
  69. package/scripts/harness-goal-init.sh +0 -72
  70. package/scripts/harness-goal-show.sh +0 -37
  71. package/scripts/harness-gotcha-memory.sh +0 -348
  72. package/scripts/harness-monitor.sh +0 -398
  73. package/scripts/harness-prompt-history.sh +0 -164
  74. package/scripts/harness-tmux.sh +0 -372
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # harness-user-prompt-submit.sh — UserPromptSubmit hook (v5 unified)
3
- # 핵심 상태 + 라우팅 지시만 주입. 회사 루프 기본, solo는 비상용 override.
3
+ # 핵심 상태 + 라우팅 지시만 주입. 회사 루프는 항상 병렬/자율 진행.
4
4
  set -e
5
5
 
6
6
  INPUT=$(cat)
@@ -102,41 +102,23 @@ EOF
102
102
  exit 0
103
103
  fi
104
104
 
105
- # ── Mode 기반 분기 ──
106
- MODE="team"
107
- if [ -f "$CWD/.harness/progress.json" ] && command -v jq >/dev/null 2>&1; then
108
- MODE=$(jq -r '.mode // "team"' "$CWD/.harness/progress.json" 2>/dev/null || echo "team")
109
- fi
110
-
111
- if [ "$MODE" = "team" ]; then
112
- FEATURE_QUEUE="$CWD/.harness/actions/feature-queue.json"
113
- T_PASSED=0; T_TOTAL=0; T_FAILED=0
114
- if [ -f "$FEATURE_QUEUE" ]; then
115
- T_PASSED=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
116
- 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)
117
- T_FAILED=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
118
- fi
119
-
120
- cat <<EOF
121
- [harness] team | ${T_PASSED}/${T_TOTAL} passed | ${T_FAILED} failed
122
-
123
- ## Team Mode Active
124
- - worker pool(기본 3)이 자율적으로 Gen→Eval 루프 실행 중 (max 5 retries)
125
- - CEO/Meeting/CTO/CQO/Service-Ops control-plane은 이 상한에 포함되지 않음
126
- - 역할: 모니터링, 실패 대응, Goal 점검
127
- - /harness-stop → 중단 | /harness-solo → 비상용 단일 에이전트 fallback
128
- - skip: "harness skip" 시 일반 대화
129
- EOF
130
- exit 0
105
+ FEATURE_QUEUE="$CWD/.harness/actions/feature-queue.json"
106
+ T_PASSED=0; T_TOTAL=0; T_FAILED=0
107
+ if [ -f "$FEATURE_QUEUE" ]; then
108
+ T_PASSED=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
109
+ 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)
110
+ T_FAILED=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
131
111
  fi
132
112
 
133
- # ── Compact context 주입 (solo/paused mode) ──
134
113
  cat <<EOF
135
- [harness] S${SPRINT_NUM} | ${PIPELINE} | agent=${CURRENT_AGENT} (${AGENT_STATUS}) | next=${NEXT_AGENT}
114
+ [harness] company | S${SPRINT_NUM} | ${PIPELINE} | agent=${CURRENT_AGENT} (${AGENT_STATUS}) | next=${NEXT_AGENT} | queue=${T_PASSED}/${T_TOTAL} passed | failed=${T_FAILED}
136
115
  ${CONTEXT_WARNING}
137
116
  ## Route
138
117
  - pipeline=none/dispatcher 미실행 → harness-dispatcher 스킬 호출
139
118
  - 기본 경로는 conductor 중심 회사 루프다: dispatch -> CEO meeting -> planner(COO) -> cto -> gen/eval -> cqo -> service-ops -> batch meeting
119
+ - worker pool(기본 3)이 자율적으로 Gen→Eval 루프 실행 중이며 control-plane은 이 상한에 포함되지 않는다
120
+ - Owner에게 "계속 진행", "진행할까요?", "다음 명령을 입력하세요"를 요구하지 않는다. GOAL이 있고 escalation이 아니면 즉시 next_agent/current_agent 업무를 수행한다
121
+ - Owner 입력은 목표 변경·사고·결과 확인용이지 회사 진행을 펌프하는 신호가 아니다
140
122
  - conductor 는 Planner 직행 대신 meeting-manager / cto / cqo / service-ops 로 next_agent를 재작성할 수 있다
141
123
  - 기능 요청 → pipeline flow | 실수 지적 → gotcha flow | 메타 질문 → 짧게 응답 (skip)
142
124
  - 활성 pipeline → next_agent/current_agent 컨텍스트로 계속
@@ -0,0 +1,179 @@
1
+ #!/bin/bash
2
+ # harness-wake-install.sh — launchd hourly wake 설치/관리
3
+ #
4
+ # 사용법:
5
+ # bash scripts/harness-wake-install.sh install [project-root...]
6
+ # bash scripts/harness-wake-install.sh add <project-root>
7
+ # bash scripts/harness-wake-install.sh remove <project-root>
8
+ # bash scripts/harness-wake-install.sh list
9
+ # bash scripts/harness-wake-install.sh status
10
+ # bash scripts/harness-wake-install.sh uninstall
11
+ #
12
+ # install: 등록된 프로젝트를 합치고 launchd job 을 (재)로드. 추가 인자가 있으면 그 경로들을 새로 추가.
13
+ # add/remove: ~/.walwal-harness/projects.list 만 수정.
14
+ # list: 현재 등록 목록 출력.
15
+ # status: launchd 상태 확인.
16
+ # uninstall: plist 언로드 + 제거.
17
+
18
+ set -uo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
+ WAKE_SH="$SCRIPT_DIR/harness-wake.sh"
22
+ HARNESS_DIR="$HOME/.walwal-harness"
23
+ PROJECTS_LIST="$HARNESS_DIR/projects.list"
24
+ LOG_DIR="$HARNESS_DIR/logs"
25
+ LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
26
+ PLIST_NAME="com.walwal.harness-wake"
27
+ PLIST_PATH="$LAUNCH_AGENTS_DIR/$PLIST_NAME.plist"
28
+
29
+ # 템플릿 위치 후보 — walwal-harness 패키지 안일 수도, 프로젝트 안일 수도.
30
+ detect_template() {
31
+ local candidates=(
32
+ "$SCRIPT_DIR/../assets/launchd/com.walwal.harness-wake.plist.template"
33
+ "$HOME/project/walwal-harness/assets/launchd/com.walwal.harness-wake.plist.template"
34
+ )
35
+ for c in "${candidates[@]}"; do
36
+ [ -f "$c" ] && { echo "$c"; return 0; }
37
+ done
38
+ return 1
39
+ }
40
+
41
+ ensure_dirs() {
42
+ mkdir -p "$HARNESS_DIR" "$LOG_DIR" "$LAUNCH_AGENTS_DIR"
43
+ touch "$PROJECTS_LIST"
44
+ }
45
+
46
+ abs_path() {
47
+ local p="$1"
48
+ if [ -d "$p" ]; then
49
+ (cd "$p" && pwd)
50
+ else
51
+ echo "$p"
52
+ fi
53
+ }
54
+
55
+ cmd_add() {
56
+ ensure_dirs
57
+ local project
58
+ project=$(abs_path "$1")
59
+ if grep -Fxq "$project" "$PROJECTS_LIST" 2>/dev/null; then
60
+ echo "[wake] already registered: $project"
61
+ return 0
62
+ fi
63
+ echo "$project" >> "$PROJECTS_LIST"
64
+ echo "[wake] added: $project"
65
+ }
66
+
67
+ cmd_remove() {
68
+ ensure_dirs
69
+ local project
70
+ project=$(abs_path "$1")
71
+ if ! grep -Fxq "$project" "$PROJECTS_LIST" 2>/dev/null; then
72
+ echo "[wake] not registered: $project"
73
+ return 0
74
+ fi
75
+ local tmp
76
+ tmp=$(mktemp)
77
+ grep -Fxv "$project" "$PROJECTS_LIST" > "$tmp" || true
78
+ mv "$tmp" "$PROJECTS_LIST"
79
+ echo "[wake] removed: $project"
80
+ }
81
+
82
+ cmd_list() {
83
+ ensure_dirs
84
+ if [ ! -s "$PROJECTS_LIST" ]; then
85
+ echo "(empty)"
86
+ return 0
87
+ fi
88
+ cat "$PROJECTS_LIST"
89
+ }
90
+
91
+ cmd_status() {
92
+ if [ -f "$PLIST_PATH" ]; then
93
+ echo "[wake] plist: $PLIST_PATH (exists)"
94
+ else
95
+ echo "[wake] plist: $PLIST_PATH (NOT installed)"
96
+ fi
97
+ if launchctl list 2>/dev/null | grep -q "$PLIST_NAME"; then
98
+ echo "[wake] launchd: loaded"
99
+ launchctl list | awk -v n="$PLIST_NAME" '$3==n {print " pid="$1" exit="$2" label="$3}'
100
+ else
101
+ echo "[wake] launchd: NOT loaded"
102
+ fi
103
+ echo "[wake] projects:"
104
+ cmd_list | sed 's/^/ /'
105
+ }
106
+
107
+ cmd_install() {
108
+ ensure_dirs
109
+
110
+ # 추가 인자가 있으면 add
111
+ shift_count=0
112
+ for p in "$@"; do
113
+ cmd_add "$p"
114
+ done
115
+
116
+ local template
117
+ template=$(detect_template) || {
118
+ echo "[wake] ERROR: plist template not found" >&2
119
+ return 1
120
+ }
121
+
122
+ # plist 생성
123
+ sed \
124
+ -e "s|{{WAKE_SCRIPT}}|$WAKE_SH|g" \
125
+ -e "s|{{LOG_DIR}}|$LOG_DIR|g" \
126
+ "$template" > "$PLIST_PATH"
127
+
128
+ # 기존 jobs 언로드 (idempotent)
129
+ launchctl unload "$PLIST_PATH" 2>/dev/null || true
130
+
131
+ # 로드
132
+ if launchctl load "$PLIST_PATH" 2>/dev/null; then
133
+ echo "[wake] launchd loaded: $PLIST_PATH"
134
+ else
135
+ echo "[wake] WARN: launchctl load 실패. 다음 명령으로 직접 시도해 보세요:" >&2
136
+ echo " launchctl load $PLIST_PATH" >&2
137
+ fi
138
+
139
+ cmd_status
140
+ }
141
+
142
+ cmd_uninstall() {
143
+ if [ -f "$PLIST_PATH" ]; then
144
+ launchctl unload "$PLIST_PATH" 2>/dev/null || true
145
+ rm -f "$PLIST_PATH"
146
+ echo "[wake] uninstalled: $PLIST_PATH"
147
+ else
148
+ echo "[wake] not installed"
149
+ fi
150
+ }
151
+
152
+ cmd_run_now() {
153
+ bash "$WAKE_SH"
154
+ echo "[wake] run-now done. log:"
155
+ tail -20 "$LOG_DIR/wake.log" 2>/dev/null || true
156
+ }
157
+
158
+ case "${1:-}" in
159
+ install) shift; cmd_install "$@" ;;
160
+ add) shift; cmd_add "${1:-$PWD}" ;;
161
+ remove) shift; cmd_remove "${1:-$PWD}" ;;
162
+ list) cmd_list ;;
163
+ status) cmd_status ;;
164
+ uninstall) cmd_uninstall ;;
165
+ run-now) cmd_run_now ;;
166
+ *)
167
+ cat <<USAGE
168
+ harness-wake-install.sh — launchd hourly wake 관리
169
+
170
+ install [project-root...] plist 생성 + 로드 (+ 프로젝트 등록)
171
+ add <project-root> 프로젝트 등록만
172
+ remove <project-root> 프로젝트 등록 해제
173
+ list 등록된 프로젝트 출력
174
+ status launchd / plist / 등록 상태
175
+ run-now 지금 즉시 wake 실행 (테스트)
176
+ uninstall plist 언로드 + 제거
177
+ USAGE
178
+ ;;
179
+ esac
@@ -0,0 +1,258 @@
1
+ #!/bin/bash
2
+ # harness-wake.sh — hourly safety wake (v6.2)
3
+ #
4
+ # launchd 가 1시간 주기로 호출. 등록된 모든 walwal-harness 프로젝트에 대해:
5
+ # 1) idle 한 시간이 임계치 이상이면 (.harness/progress.json mtime 기준, review 실행 전 캡처)
6
+ # 2) deterministic hourly review 를 디스크에 남김
7
+ # 3) 기본값으로 기존 tmux 세션에 긴 prompt 를 주입하지 않고 headless 1-tick Claude 를 실행
8
+ #
9
+ # 등록 방식: ~/.walwal-harness/projects.list 에 한 줄에 하나씩 절대경로 기재.
10
+ # 또는 환경변수 WALWAL_HARNESS_PROJECTS 에 콜론(:)으로 구분된 절대경로 목록.
11
+
12
+ set -uo pipefail
13
+
14
+ LOG_DIR="${HOME}/.walwal-harness/logs"
15
+ mkdir -p "$LOG_DIR"
16
+ LOG="$LOG_DIR/wake.log"
17
+
18
+ ts() { date "+%Y-%m-%d %H:%M:%S"; }
19
+ say() { echo "[$(ts)] $*" >> "$LOG"; }
20
+
21
+ stat_mtime() {
22
+ local path="$1"
23
+ stat -f "%m" "$path" 2>/dev/null || stat -c "%Y" "$path" 2>/dev/null
24
+ }
25
+
26
+ in_force_wake_window() {
27
+ local hhmm start end
28
+ hhmm="$(date "+%H%M")"
29
+ start="${HARNESS_WAKE_FORCE_START_HHMM:-0555}"
30
+ end="${HARNESS_WAKE_FORCE_END_HHMM:-0615}"
31
+
32
+ # 기본값은 Anthropic 5-hour reset 이 자주 걸리는 06:00 KST 주변.
33
+ # StartInterval=3600 이 몇 분 밀려 실행돼도 복구되도록 20분 창을 둔다.
34
+ [ "$((10#$hhmm))" -ge "$((10#$start))" ] && [ "$((10#$hhmm))" -le "$((10#$end))" ]
35
+ }
36
+
37
+ find_tmux_session() {
38
+ local project_root="$1"
39
+ local base sanitized upper var cand
40
+
41
+ base=$(basename "$project_root")
42
+ sanitized=$(echo "$base" | tr -- '-. ' '___')
43
+ for cand in "claude_${sanitized}_" "claude_${base}_"; do
44
+ if tmux has-session -t "$cand" 2>/dev/null; then
45
+ echo "$cand"
46
+ return 0
47
+ fi
48
+ done
49
+
50
+ upper=$(echo "$sanitized" | tr 'a-z' 'A-Z')
51
+ var="WALWAL_HARNESS_TMUX_${upper}"
52
+ if [ -n "${!var:-}" ] && tmux has-session -t "${!var}" 2>/dev/null; then
53
+ echo "${!var}"
54
+ return 0
55
+ fi
56
+
57
+ return 1
58
+ }
59
+
60
+ start_headless_tick() {
61
+ local project_root="$1"
62
+ local idle="$2"
63
+ local force="$3"
64
+ local review_path="$4"
65
+ local mode stamp wake_dir ops_dir prompt_rel log_rel prompt_path log_path pid_file old_pid pid status tmux_session
66
+
67
+ mode="${HARNESS_WAKE_MODE:-headless}"
68
+ stamp="$(date -u "+%Y%m%dT%H%M%SZ")"
69
+ wake_dir="$project_root/.harness/actions/wake"
70
+ ops_dir="$project_root/.harness/ops/wake"
71
+ mkdir -p "$wake_dir" "$ops_dir"
72
+
73
+ prompt_rel=".harness/actions/wake/wake-${stamp}.prompt.md"
74
+ log_rel=".harness/ops/wake/wake-${stamp}.log"
75
+ prompt_path="$project_root/$prompt_rel"
76
+ log_path="$project_root/$log_rel"
77
+ pid_file="$ops_dir/wake.pid"
78
+
79
+ if [ -f "$pid_file" ]; then
80
+ old_pid="$(cat "$pid_file" 2>/dev/null || true)"
81
+ if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
82
+ say "skip-headless: $project_root (previous wake pid=$old_pid still running)"
83
+ return 0
84
+ fi
85
+ fi
86
+
87
+ cat > "$prompt_path" <<EOF
88
+ You are the walwal-harness autonomous hourly wake tick.
89
+
90
+ Project root: $project_root
91
+ Wake reason: idle=${idle}s force=${force}
92
+ Hourly review: ${review_path:-"(not available)"}
93
+
94
+ Policy:
95
+ - Owner input after the initial GOAL is an interrupt or additional request, never a reason to wait.
96
+ - Never set conductor.state to waiting_owner. Normalize any waiting_owner state to waiting_meeting and continue.
97
+ - meeting-manager is the default synchronization loop. Incidents, service warnings, goal drift, and escalations go to meeting-manager first.
98
+ - Keep this to one bounded autonomous tick. Do not run an endless loop.
99
+ - Do not write future timestamps to .harness/progress.log or artifacts.
100
+ - Keep terminal output concise; write evidence to project files.
101
+
102
+ Tasks:
103
+ 1. Read CONVENTIONS.md if present, .harness/conventions/shared.md, relevant .harness/conventions/<role>.md, .harness/gotchas/<role>.md, .harness/memory.md, .harness/progress.json, and the hourly review above.
104
+ 2. If service_ops.requested_mode is monitor, run or trigger one service-ops monitor pass and record last_check. stream_active may be true only during the pass and must be false when finished.
105
+ 3. Treat the hourly review as Executive Meeting Minutes. Verify it contains CEO/COO/CTO/CQO/Service-Ops role positions, discussion, decision JSON, and action items.
106
+ 4. Check conductor state against the current GOAL and route the next department without waiting for Owner.
107
+ 5. If there is an active incident or operational warning, make sure meeting-manager has shared context and an action decision that names CTO/CQO/Service-Ops responsibilities.
108
+ 6. Append a short factual summary to .harness/progress.log and any normal harness artifacts used by this project.
109
+ EOF
110
+
111
+ case "$mode" in
112
+ record)
113
+ status="recorded"
114
+ ;;
115
+ headless|"")
116
+ if command -v claude >/dev/null 2>&1; then
117
+ (
118
+ cd "$project_root" || exit 1
119
+ claude -p "$(cat "$prompt_path")" > "$log_path" 2>&1
120
+ ) &
121
+ pid=$!
122
+ echo "$pid" > "$pid_file"
123
+ status="spawned pid=$pid"
124
+ else
125
+ status="recorded claude-not-found"
126
+ fi
127
+ ;;
128
+ *)
129
+ status="recorded unsupported-mode=$mode"
130
+ ;;
131
+ esac
132
+
133
+ echo "$(date -u "+%Y-%m-%dT%H:%M:%SZ") | wake | headless | $status | idle=${idle}s | force=${force} | prompt=$prompt_rel | log=$log_rel" >> "$project_root/.harness/progress.log"
134
+ say "wake-headless: $project_root (idle ${idle}s, force=${force}, mode=${mode}) → $status $log_rel"
135
+
136
+ if tmux_session="$(find_tmux_session "$project_root" 2>/dev/null)"; then
137
+ tmux display-message -t "$tmux_session" "walwal wake headless tick: $status ($log_rel)" 2>/dev/null || true
138
+ fi
139
+ }
140
+
141
+ PROJECTS_LIST="${HOME}/.walwal-harness/projects.list"
142
+ PROJECTS=()
143
+
144
+ if [ -n "${WALWAL_HARNESS_PROJECTS:-}" ]; then
145
+ IFS=':' read -r -a PROJECTS <<< "$WALWAL_HARNESS_PROJECTS"
146
+ elif [ -f "$PROJECTS_LIST" ]; then
147
+ while IFS= read -r line; do
148
+ line="${line%%#*}"
149
+ line="$(echo "$line" | xargs)"
150
+ [ -z "$line" ] && continue
151
+ PROJECTS+=("$line")
152
+ done < "$PROJECTS_LIST"
153
+ fi
154
+
155
+ if [ "${#PROJECTS[@]}" -eq 0 ]; then
156
+ say "no registered projects (set ~/.walwal-harness/projects.list or WALWAL_HARNESS_PROJECTS)"
157
+ exit 0
158
+ fi
159
+
160
+ # 임계치: idle 60분 이상이면 깨운다 (Stop 훅이 정상 동작하면 거의 연속 활동이므로
161
+ # 1시간 이상 정지 = 어디선가 멈춘 상태로 판단)
162
+ IDLE_THRESHOLD_SECONDS="${HARNESS_WAKE_IDLE_SECONDS:-3300}" # 55분 (안전 마진)
163
+ NOW_EPOCH=$(date "+%s")
164
+ FORCE_WAKE=0
165
+ if in_force_wake_window; then
166
+ FORCE_WAKE=1
167
+ fi
168
+
169
+ for PROJECT_ROOT in "${PROJECTS[@]}"; do
170
+ if [ ! -d "$PROJECT_ROOT" ]; then
171
+ say "skip: $PROJECT_ROOT (not a directory)"
172
+ continue
173
+ fi
174
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
175
+ if [ ! -f "$PROGRESS" ]; then
176
+ say "skip: $PROJECT_ROOT (no .harness/progress.json)"
177
+ continue
178
+ fi
179
+ PROJECT_FORCE_WAKE="$FORCE_WAKE"
180
+
181
+ if command -v jq >/dev/null 2>&1 && [ "$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo idle)" = "waiting_owner" ]; then
182
+ if [ -x "$PROJECT_ROOT/scripts/harness-progress-set.sh" ]; then
183
+ bash "$PROJECT_ROOT/scripts/harness-progress-set.sh" "$PROJECT_ROOT" \
184
+ '.conductor.state = "waiting_meeting" |
185
+ .conductor.current_action = "autonomous-normalize-waiting-owner" |
186
+ .next_agent = "meeting-manager" |
187
+ .agent_status = "pending" |
188
+ .meetings.active = ((.meetings.active // []) + ["meeting-manager"] | unique) |
189
+ .meetings.requested_type = (.meetings.requested_type // "followup-review") |
190
+ .meetings.requested_reason = (.meetings.requested_reason // "autonomous-normalize-waiting-owner") |
191
+ .workflow.stage = "ops-monitoring" |
192
+ .workflow.last_transition = (now | todate) |
193
+ .workflow.last_reason = "autonomous-normalize-waiting-owner"' \
194
+ >/dev/null 2>&1 || true
195
+ say "normalize: $PROJECT_ROOT (conductor=waiting_owner → waiting_meeting)"
196
+ PROJECT_FORCE_WAKE=1
197
+ fi
198
+ fi
199
+
200
+ # idle 시간은 hourly-review 실행 전에 캡처한다. review 는 progress.json 과
201
+ # progress.log 를 갱신하므로, 이후 mtime 을 보면 wake 가 자기 활동을
202
+ # "프로젝트 활동 중"으로 오판한다.
203
+ if MTIME=$(stat_mtime "$PROGRESS"); then :;
204
+ else
205
+ say "skip: $PROJECT_ROOT (cannot stat progress.json)"
206
+ continue
207
+ fi
208
+ IDLE=$((NOW_EPOCH - MTIME))
209
+
210
+ # Every hourly batch must leave disk-backed evidence, even if Claude/tmux is
211
+ # stopped or busy. This deterministic review runs after the idle snapshot.
212
+ if [ -x "$PROJECT_ROOT/scripts/harness-hourly-review.sh" ]; then
213
+ review_output=$(bash "$PROJECT_ROOT/scripts/harness-hourly-review.sh" "$PROJECT_ROOT" 2>/dev/null || true)
214
+ review_path=$(printf "%s\n" "$review_output" | grep -Eo '(/[^[:space:]]+|[.][^[:space:]]+)\.md' | tail -1)
215
+ if [ -z "$review_path" ]; then
216
+ review_path=$(printf "%s\n" "$review_output" | tail -1)
217
+ fi
218
+ if [ -n "$review_path" ]; then
219
+ say "review: $PROJECT_ROOT → $review_path"
220
+ else
221
+ say "review-fail: $PROJECT_ROOT"
222
+ fi
223
+ fi
224
+
225
+ if [ "$IDLE" -lt "$IDLE_THRESHOLD_SECONDS" ] && [ "$PROJECT_FORCE_WAKE" -ne 1 ]; then
226
+ say "skip: $PROJECT_ROOT (idle ${IDLE}s < threshold ${IDLE_THRESHOLD_SECONDS}s — 활동 중)"
227
+ continue
228
+ fi
229
+
230
+ # 회사 루프가 멈춘 상태인지 확인
231
+ if command -v jq >/dev/null 2>&1; then
232
+ CSTATE=$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo "idle")
233
+ SSTATUS=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null || echo "init")
234
+ if [ "$SSTATUS" = "completed" ] || [ "$SSTATUS" = "aborted" ]; then
235
+ say "continue: $PROJECT_ROOT (sprint=$SSTATUS — autonomous review still required)"
236
+ fi
237
+ if [ "$CSTATE" = "paused" ] || [ "$CSTATE" = "completed" ] || [ "$CSTATE" = "escalated" ]; then
238
+ say "continue: $PROJECT_ROOT (conductor=$CSTATE — route through meeting-manager/service-ops)"
239
+ fi
240
+ fi
241
+
242
+ # 발화 직전 service-ops monitor 트리거 partial update.
243
+ # requested_mode 만 set — conductor-tick 이 이걸 보고 service-ops 를 spawn 한다.
244
+ # stream_active 는 service-ops 본인이 spawn 시 §3.1.1 에 따라 직접 set (정직성 룰).
245
+ # auto-retro / incident / null / monitor 가 아닌 모드가 진행 중이면 덮어쓰지 않음.
246
+ if [ -x "$PROJECT_ROOT/scripts/harness-progress-set.sh" ]; then
247
+ bash "$PROJECT_ROOT/scripts/harness-progress-set.sh" "$PROJECT_ROOT" \
248
+ 'if (.service_ops.requested_mode // "") == "" or (.service_ops.requested_mode // "") == "monitor" or (.service_ops.requested_mode // "") == "auto-retro"
249
+ then .service_ops.requested_mode = "monitor" |
250
+ .service_ops.monitor.wake_trigger_at = (now | todate)
251
+ else . end' \
252
+ >/dev/null 2>&1 || true
253
+ fi
254
+
255
+ start_headless_tick "$PROJECT_ROOT" "$IDLE" "$PROJECT_FORCE_WAKE" "$review_path"
256
+ done
257
+
258
+ exit 0
@@ -0,0 +1,157 @@
1
+ #!/bin/bash
2
+ # harness-worker-dispatch.sh — connect feature queue to actual worker launches
3
+ #
4
+ # Deterministic responsibilities:
5
+ # 1. auto-dispatch ready features to idle worker slots
6
+ # 2. create per-worker prompt + log paths
7
+ # 3. optionally launch `claude -p` workers in background
8
+ # 4. write progress.json.company_state so dashboards/CLI show real activity
9
+
10
+ set -uo pipefail
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
14
+
15
+ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || exit 0
16
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
17
+ CONFIG="$PROJECT_ROOT/.harness/config.json"
18
+ FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
19
+ QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
20
+ WORKER_DIR="$PROJECT_ROOT/.harness/actions/workers"
21
+ LOG_DIR="$PROJECT_ROOT/.harness/ops/workers"
22
+
23
+ [ -f "$PROGRESS" ] || exit 0
24
+ [ -f "$CONFIG" ] || exit 0
25
+ [ -f "$FEATURES" ] || exit 0
26
+ [ -x "$SCRIPT_DIR/harness-queue-manager.sh" ] || exit 0
27
+ command -v jq >/dev/null 2>&1 || exit 0
28
+
29
+ mkdir -p "$WORKER_DIR" "$LOG_DIR"
30
+
31
+ spawn_mode="$(jq -r '.company_mode.worker_spawn // "claude"' "$CONFIG" 2>/dev/null || echo claude)"
32
+ pipeline="$(jq -r '.pipeline // "FULLSTACK"' "$PROGRESS" 2>/dev/null || echo FULLSTACK)"
33
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
34
+ stamp="$(date -u +%Y%m%dT%H%M%SZ)"
35
+
36
+ agent_for_feature() {
37
+ local fid="$1"
38
+ local layer service title
39
+ layer="$(jq -r --arg id "$fid" '.features[]? | select(.id == $id) | (.layer // .type // "")' "$FEATURES" 2>/dev/null | head -1)"
40
+ service="$(jq -r --arg id "$fid" '.features[]? | select(.id == $id) | (.service // "")' "$FEATURES" 2>/dev/null | head -1)"
41
+ title="$(jq -r --arg id "$fid" '.features[]? | select(.id == $id) | (.title // .name // .description // "")' "$FEATURES" 2>/dev/null | head -1)"
42
+ case "$(printf '%s %s %s' "$layer" "$service" "$title" | tr '[:upper:]' '[:lower:]')" in
43
+ *frontend*|*ui*|*web*|*react*|*next*) echo "generator-frontend" ;;
44
+ *design*|*token*|*component-spec*) echo "generator-designer" ;;
45
+ *devops*|*infra*|*deploy*|*ci*) echo "generator-devops" ;;
46
+ *)
47
+ if [ "$pipeline" = "FE-ONLY" ]; then echo "generator-frontend"; else echo "generator-backend"; fi
48
+ ;;
49
+ esac
50
+ }
51
+
52
+ build_prompt() {
53
+ local team="$1" fid="$2" agent="$3" prompt_path="$4" log_path="$5"
54
+ cat > "$prompt_path" <<EOF
55
+ You are running as walwal-harness company worker.
56
+
57
+ Agent: harness-${agent}
58
+ Worker slot: ${team}
59
+ Feature ID: ${fid}
60
+ Project root: ${PROJECT_ROOT}
61
+
62
+ Follow these rules exactly:
63
+ 1. Read AGENTS.md, CONVENTIONS.md if present, .harness/conventions/shared.md, .harness/conventions/${agent}.md if present, .harness/gotchas/${agent}.md if present, .harness/memory.md, .harness/progress.json, .harness/actions/feature-list.json, .harness/actions/api-contract.json, and the relevant harness skill for ${agent}.
64
+ 2. Work only on FEATURE_ID=${fid}. Do not broaden scope.
65
+ 3. Respect IA ownership and do not edit files outside your department.
66
+ 4. Write a report to .harness/actions/gen-report-${fid}.md if you are a generator, or .harness/actions/evaluation-${fid}.md if you are an evaluator.
67
+ 5. On success, update feature-list passes and run: bash scripts/harness-queue-manager.sh pass ${fid}
68
+ 6. On failure, write the reason and run: bash scripts/harness-queue-manager.sh fail ${fid} "<short reason>"
69
+ 7. Use partial progress updates only via scripts/harness-progress-set.sh.
70
+ 8. Append real timestamped progress to .harness/progress.log. Never write future timestamps.
71
+
72
+ This worker log is: ${log_path#$PROJECT_ROOT/}
73
+ EOF
74
+ }
75
+
76
+ if [ ! -f "$QUEUE" ]; then
77
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" init all "$PROJECT_ROOT" >/dev/null 2>&1 || exit 0
78
+ else
79
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" recover "$PROJECT_ROOT" >/dev/null 2>&1 || true
80
+ fi
81
+
82
+ pairs="$(bash "$SCRIPT_DIR/harness-queue-manager.sh" auto-dispatch "$PROJECT_ROOT" 2>/dev/null || echo "[]")"
83
+ if ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"$pairs"; then
84
+ pairs="[]"
85
+ fi
86
+
87
+ count="$(jq 'length' <<<"$pairs")"
88
+ if [ "$count" -eq 0 ]; then
89
+ active_workers="$(jq '[.teams | to_entries[] | select(.value.status == "busy") | {team:(.key|tonumber), feature:.value.feature, pid:.value.pid, phase:(.value.phase // "gen")}]' "$QUEUE" 2>/dev/null || echo "[]")"
90
+ jq --arg ts "$ts" --argjson workers "$active_workers" '
91
+ .company_state.active_workers = ($workers | length) |
92
+ .company_state.workers = $workers |
93
+ .company_state.last_dispatch_at = (.company_state.last_dispatch_at // null)
94
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
95
+ echo "[]"
96
+ exit 0
97
+ fi
98
+
99
+ dispatches_file="$(mktemp)"
100
+ : > "$dispatches_file"
101
+
102
+ i=0
103
+ while [ "$i" -lt "$count" ]; do
104
+ team="$(jq -r ".[$i].team" <<<"$pairs")"
105
+ fid="$(jq -r ".[$i].feature" <<<"$pairs")"
106
+ agent="$(agent_for_feature "$fid")"
107
+ prompt_rel=".harness/actions/workers/T${team}-${fid}-${stamp}.prompt.md"
108
+ log_rel=".harness/ops/workers/T${team}-${fid}-${stamp}.log"
109
+ prompt_path="$PROJECT_ROOT/$prompt_rel"
110
+ log_path="$PROJECT_ROOT/$log_rel"
111
+
112
+ build_prompt "$team" "$fid" "$agent" "$prompt_path" "$log_path"
113
+
114
+ pid="null"
115
+ status="recorded"
116
+ if [ "$spawn_mode" = "claude" ] && command -v claude >/dev/null 2>&1; then
117
+ (
118
+ cd "$PROJECT_ROOT" || exit 1
119
+ claude -p "$(cat "$prompt_path")" > "$log_path" 2>&1
120
+ ) &
121
+ pid="$!"
122
+ status="spawned"
123
+ fi
124
+
125
+ tmpq="${QUEUE}.tmp.$$.$i"
126
+ jq --arg tid "$team" --arg agent "$agent" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" --arg ts "$ts" '
127
+ .teams[$tid].pid = $pid |
128
+ .teams[$tid].agent = $agent |
129
+ .teams[$tid].prompt = $prompt |
130
+ .teams[$tid].log = $log |
131
+ .teams[$tid].spawn_status = $status |
132
+ .teams[$tid].started_at = $ts |
133
+ .queue.in_progress[.teams[$tid].feature].agent = $agent |
134
+ .queue.in_progress[.teams[$tid].feature].prompt = $prompt |
135
+ .queue.in_progress[.teams[$tid].feature].log = $log |
136
+ .queue.in_progress[.teams[$tid].feature].pid = $pid
137
+ ' "$QUEUE" > "$tmpq" && mv "$tmpq" "$QUEUE"
138
+
139
+ jq -n --argjson team "$team" --arg feature "$fid" --arg agent "$agent" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" \
140
+ '{team:$team, feature:$feature, agent:$agent, prompt:$prompt, log:$log, status:$status, pid:$pid}' >> "$dispatches_file"
141
+ echo "$ts | conductor | worker-dispatch | $status | T${team}/${fid} | $agent pid=$pid" >> "$PROJECT_ROOT/.harness/progress.log"
142
+ i=$((i + 1))
143
+ done
144
+
145
+ dispatches="$(jq -s '.' "$dispatches_file")"
146
+ rm -f "$dispatches_file"
147
+ active_workers="$(jq '[.teams | to_entries[] | select(.value.status == "busy") | {team:(.key|tonumber), feature:.value.feature, agent:.value.agent, pid:.value.pid, phase:(.value.phase // "gen"), prompt:.value.prompt, log:.value.log, spawn_status:.value.spawn_status}]' "$QUEUE")"
148
+
149
+ jq --arg ts "$ts" --argjson dispatches "$dispatches" --argjson workers "$active_workers" '
150
+ .company_state.active_workers = ($workers | length) |
151
+ .company_state.workers = $workers |
152
+ .company_state.last_dispatch_at = $ts |
153
+ .company_state.last_dispatch = $dispatches |
154
+ .conductor.worker_dispatches = ((.conductor.worker_dispatches // []) + $dispatches)
155
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
156
+
157
+ echo "$dispatches"
@@ -19,7 +19,7 @@ migrate_progress_schema() {
19
19
 
20
20
  # v6.2 — Parallel tracks (fork-join) fields. tracks.length >= 2 → fork.
21
21
  # Note: there is intentionally NO `meetings.decision.mode` field — single/parallel is derived
22
- # from tracks.length. Runtime progress.mode is preserved because it still governs solo/team/paused.
22
+ # from tracks.length. Runtime progress.mode is normalized to always-on company mode.
23
23
  local filter='
24
24
  .conductor.tracks = (.conductor.tracks // [])
25
25
  | .conductor.rendezvous = (.conductor.rendezvous // null)
@@ -35,6 +35,11 @@ migrate_progress_schema() {
35
35
  | .meetings.decision.rendezvous = (.meetings.decision.rendezvous // null)
36
36
  | del(.meetings.decision.mode)
37
37
  | del(.meetings.requested_mode)
38
+ | .mode = "company"
39
+ | .mode_decision.owner = "conductor"
40
+ | .mode_decision.policy = "always_company_parallel"
41
+ | .mode_decision.user_override = null
42
+ | .mode_decision.rationale = (.mode_decision.rationale // "company mode normalized by migration")
38
43
  '
39
44
 
40
45
  local tmp