@walwal-harness/cli 6.1.4 → 6.1.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +40 -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 +8 -8
  6. package/assets/templates/company-pipeline-manifest.json +71 -0
  7. package/assets/templates/config.json +29 -27
  8. package/assets/templates/memory.md +16 -2
  9. package/assets/templates/progress.json.template +6 -5
  10. package/bin/init.js +180 -90
  11. package/conventions/shared.md +34 -0
  12. package/gotchas/conductor.md +16 -0
  13. package/gotchas/dispatcher.md +16 -0
  14. package/gotchas/meeting-manager.md +8 -0
  15. package/gotchas/service-ops.md +8 -0
  16. package/package.json +4 -4
  17. package/scripts/conductor-tick.sh +157 -56
  18. package/scripts/harness-archive.sh +7 -5
  19. package/scripts/harness-dashboard-up.sh +11 -3
  20. package/scripts/harness-hourly-review.sh +582 -0
  21. package/scripts/harness-meeting-doc.sh +55 -16
  22. package/scripts/harness-next.sh +1 -1
  23. package/scripts/harness-parity-audit.sh +101 -0
  24. package/scripts/harness-queue-manager.sh +13 -6
  25. package/scripts/harness-service-ops-monitor.sh +273 -0
  26. package/scripts/harness-session-start.sh +60 -31
  27. package/scripts/harness-statusline.sh +8 -12
  28. package/scripts/harness-stop.sh +85 -0
  29. package/scripts/harness-user-prompt-submit.sh +11 -29
  30. package/scripts/harness-wake-install.sh +180 -0
  31. package/scripts/harness-wake.sh +350 -0
  32. package/scripts/harness-worker-dispatch.sh +188 -0
  33. package/scripts/lib/harness-agent-resolver.sh +127 -0
  34. package/scripts/lib/harness-progress-migrate.sh +6 -1
  35. package/scripts/lib/harness-render-progress.sh +3 -3
  36. package/skills/brainstorming/SKILL.md +2 -2
  37. package/skills/conductor/SKILL.md +67 -55
  38. package/skills/cqo/SKILL.md +1 -0
  39. package/skills/cto/SKILL.md +1 -0
  40. package/skills/dispatcher/SKILL.md +33 -14
  41. package/skills/evaluator-code-quality/SKILL.md +4 -4
  42. package/skills/evaluator-functional/SKILL.md +6 -6
  43. package/skills/evaluator-visual/SKILL.md +4 -4
  44. package/skills/generator-backend/SKILL.md +4 -4
  45. package/skills/generator-frontend/SKILL.md +5 -5
  46. package/skills/meeting-manager/SKILL.md +82 -12
  47. package/skills/planner/SKILL.md +3 -3
  48. package/skills/service-ops/SKILL.md +25 -0
  49. package/commands/harness-solo.md +0 -103
  50. package/commands/harness-stop.md +0 -53
  51. package/commands/harness-team.md +0 -530
  52. package/scripts/harness-dashboard.sh +0 -509
  53. package/scripts/harness-goal-init.sh +0 -72
  54. package/scripts/harness-goal-show.sh +0 -37
  55. package/scripts/harness-gotcha-memory.sh +0 -348
  56. package/scripts/harness-monitor.sh +0 -398
  57. package/scripts/harness-prompt-history.sh +0 -164
  58. package/scripts/harness-tmux.sh +0 -372
@@ -0,0 +1,350 @@
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
+ export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
15
+
16
+ LOG_DIR="${HOME}/.walwal-harness/logs"
17
+ mkdir -p "$LOG_DIR"
18
+ LOG="$LOG_DIR/wake.log"
19
+
20
+ ts() { date "+%Y-%m-%d %H:%M:%S"; }
21
+ say() { echo "[$(ts)] $*" >> "$LOG"; }
22
+
23
+ stat_mtime() {
24
+ local path="$1"
25
+ stat -f "%m" "$path" 2>/dev/null || stat -c "%Y" "$path" 2>/dev/null
26
+ }
27
+
28
+ in_force_wake_window() {
29
+ local hhmm start end
30
+ hhmm="$(date "+%H%M")"
31
+ start="${HARNESS_WAKE_FORCE_START_HHMM:-0555}"
32
+ end="${HARNESS_WAKE_FORCE_END_HHMM:-0615}"
33
+
34
+ # 기본값은 Anthropic 5-hour reset 이 자주 걸리는 06:00 KST 주변.
35
+ # StartInterval=3600 이 몇 분 밀려 실행돼도 복구되도록 20분 창을 둔다.
36
+ [ "$((10#$hhmm))" -ge "$((10#$start))" ] && [ "$((10#$hhmm))" -le "$((10#$end))" ]
37
+ }
38
+
39
+ find_tmux_session() {
40
+ local project_root="$1"
41
+ local base sanitized upper var cand
42
+
43
+ base=$(basename "$project_root")
44
+ sanitized=$(echo "$base" | tr -- '-. ' '___')
45
+ for cand in "claude_${sanitized}_" "claude_${base}_"; do
46
+ if tmux has-session -t "$cand" 2>/dev/null; then
47
+ echo "$cand"
48
+ return 0
49
+ fi
50
+ done
51
+
52
+ upper=$(echo "$sanitized" | tr 'a-z' 'A-Z')
53
+ var="WALWAL_HARNESS_TMUX_${upper}"
54
+ if [ -n "${!var:-}" ] && tmux has-session -t "${!var}" 2>/dev/null; then
55
+ echo "${!var}"
56
+ return 0
57
+ fi
58
+
59
+ return 1
60
+ }
61
+
62
+ resolve_agent_bin() {
63
+ local executor="$1"
64
+ local env_name cand
65
+
66
+ case "$executor" in
67
+ codex)
68
+ env_name="${HARNESS_CODEX_BIN:-${CODEX_BIN:-}}"
69
+ ;;
70
+ claude|*)
71
+ env_name="${HARNESS_CLAUDE_BIN:-${CLAUDE_BIN:-}}"
72
+ executor="claude"
73
+ ;;
74
+ esac
75
+
76
+ for cand in "$env_name" "$HOME/.local/bin/$executor" "/opt/homebrew/bin/$executor" "/usr/local/bin/$executor"; do
77
+ [ -n "$cand" ] && [ -x "$cand" ] && { echo "$cand"; return 0; }
78
+ done
79
+
80
+ command -v "$executor" 2>/dev/null && return 0
81
+ return 1
82
+ }
83
+
84
+ run_conductor_fallback() {
85
+ local project_root="$1"
86
+ local reason="$2"
87
+ local tick="$project_root/scripts/conductor-tick.sh"
88
+ local progress="$project_root/.harness/progress.json"
89
+ local progress_set="$project_root/scripts/harness-progress-set.sh"
90
+ local before after status
91
+
92
+ [ "${HARNESS_WAKE_CONDUCTOR_FALLBACK:-1}" = "1" ] || return 0
93
+ [ -x "$tick" ] || return 0
94
+ [ -f "$progress" ] || return 0
95
+ command -v jq >/dev/null 2>&1 || return 0
96
+
97
+ before="$(jq -c '{current_agent,next_agent,agent_status,conductor:(.conductor.state // null),decision:(.meetings.decision // {})}' "$progress" 2>/dev/null || echo '{}')"
98
+
99
+ if [ -x "$progress_set" ] &&
100
+ jq -e '
101
+ (.meetings.decision.owner // "") != "" and
102
+ ((((.meetings.active // []) | length) > 0) or ((.meetings.decision.required_execution // null) != null))
103
+ ' "$progress" >/dev/null 2>&1 && {
104
+ [ "$(jq -r '.current_agent // ""' "$progress")" != "meeting-manager" ] ||
105
+ [ "$(jq -r '.agent_status // ""' "$progress")" != "completed" ];
106
+ }; then
107
+ bash "$progress_set" "$project_root" '
108
+ .current_agent = "meeting-manager" |
109
+ .agent_status = "completed" |
110
+ .conductor.state = "running" |
111
+ .conductor.current_action = "wake-meeting-decision-ready" |
112
+ .workflow.last_transition = (now | todate) |
113
+ .workflow.last_reason = "wake-meeting-decision-ready"
114
+ ' >/dev/null 2>&1 || true
115
+ fi
116
+
117
+ if bash "$tick" "$project_root" >/dev/null 2>&1; then
118
+ status="ok"
119
+ else
120
+ status="failed"
121
+ fi
122
+
123
+ after="$(jq -c '{current_agent,next_agent,agent_status,conductor:(.conductor.state // null),last_action:(.conductor.current_action // null)}' "$progress" 2>/dev/null || echo '{}')"
124
+ echo "$(date -u "+%Y-%m-%dT%H:%M:%SZ") | wake | conductor-fallback | $status | reason=$reason | before=$before | after=$after" >> "$project_root/.harness/progress.log"
125
+ say "conductor-fallback: $project_root ($status, reason=$reason)"
126
+ }
127
+
128
+ start_headless_tick() {
129
+ local project_root="$1"
130
+ local idle="$2"
131
+ local force="$3"
132
+ local review_path="$4"
133
+ local mode executor agent_bin stamp wake_dir ops_dir prompt_rel log_rel prompt_path log_path pid_file old_pid pid status tmux_session session_name session_safe
134
+
135
+ mode="${HARNESS_WAKE_MODE:-headless}"
136
+ executor="${HARNESS_WAKE_EXECUTOR:-claude}"
137
+ stamp="$(date -u "+%Y%m%dT%H%M%SZ")"
138
+ wake_dir="$project_root/.harness/actions/wake"
139
+ ops_dir="$project_root/.harness/ops/wake"
140
+ mkdir -p "$wake_dir" "$ops_dir"
141
+
142
+ prompt_rel=".harness/actions/wake/wake-${stamp}.prompt.md"
143
+ log_rel=".harness/ops/wake/wake-${stamp}.log"
144
+ prompt_path="$project_root/$prompt_rel"
145
+ log_path="$project_root/$log_rel"
146
+ pid_file="$ops_dir/wake.pid"
147
+
148
+ if [ -f "$pid_file" ]; then
149
+ old_pid="$(cat "$pid_file" 2>/dev/null || true)"
150
+ if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
151
+ say "skip-headless: $project_root (previous wake pid=$old_pid still running)"
152
+ return 0
153
+ fi
154
+ fi
155
+
156
+ cat > "$prompt_path" <<EOF
157
+ You are the walwal-harness autonomous hourly wake tick.
158
+
159
+ Project root: $project_root
160
+ Wake reason: idle=${idle}s force=${force}
161
+ Hourly review: ${review_path:-"(not available)"}
162
+
163
+ Policy:
164
+ - Owner input after the initial GOAL is an interrupt or additional request, never a reason to wait.
165
+ - Never set conductor.state to waiting_owner. Normalize any waiting_owner state to waiting_meeting and continue.
166
+ - meeting-manager is the default synchronization loop. Incidents, service warnings, goal drift, and escalations go to meeting-manager first.
167
+ - Keep this to one bounded autonomous tick. Do not run an endless loop.
168
+ - Do not write future timestamps to .harness/progress.log or artifacts.
169
+ - Keep terminal output concise; write evidence to project files.
170
+
171
+ Tasks:
172
+ 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.
173
+ 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.
174
+ 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.
175
+ 4. Check conductor state against the current GOAL and route the next department without waiting for Owner.
176
+ 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.
177
+ 6. Split meaningful progress from paperwork-only artifacts in your summary.
178
+ 7. Append a short factual summary to .harness/progress.log and any normal harness artifacts used by this project.
179
+ EOF
180
+
181
+ case "$mode" in
182
+ record)
183
+ status="recorded"
184
+ ;;
185
+ headless|"")
186
+ if agent_bin="$(resolve_agent_bin "$executor")"; then
187
+ (
188
+ cd "$project_root" || exit 1
189
+ if [ "$executor" = "codex" ]; then
190
+ "$agent_bin" exec -C "$project_root" "$(cat "$prompt_path")" > "$log_path" 2>&1
191
+ else
192
+ "$agent_bin" -p "$(cat "$prompt_path")" > "$log_path" 2>&1
193
+ fi
194
+ ) &
195
+ pid=$!
196
+ echo "$pid" > "$pid_file"
197
+ status="spawned executor=$executor pid=$pid"
198
+ else
199
+ status="BLOCKED_RUNTIME ${executor}-not-found"
200
+ fi
201
+ ;;
202
+ tmux)
203
+ if ! command -v tmux >/dev/null 2>&1; then
204
+ status="BLOCKED_RUNTIME tmux-not-found"
205
+ elif agent_bin="$(resolve_agent_bin "$executor")"; then
206
+ session_safe="$(basename "$project_root" | tr -c '[:alnum:]_' '_')"
207
+ session_name="walwal_${session_safe}_${stamp}"
208
+ if [ "$executor" = "codex" ]; then
209
+ tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' exec -C '$project_root' - < '$prompt_path' 2>&1 | tee '$log_path'"
210
+ else
211
+ tmux new-session -d -s "$session_name" "cd '$project_root' && '$agent_bin' -p \"\$(cat '$prompt_path')\" 2>&1 | tee '$log_path'"
212
+ fi
213
+ status="spawned-tmux executor=$executor session=$session_name"
214
+ else
215
+ status="BLOCKED_RUNTIME ${executor}-not-found"
216
+ fi
217
+ ;;
218
+ *)
219
+ status="recorded unsupported-mode=$mode"
220
+ ;;
221
+ esac
222
+
223
+ echo "$(date -u "+%Y-%m-%dT%H:%M:%SZ") | wake | agent-tick | $status | idle=${idle}s | force=${force} | mode=${mode} | prompt=$prompt_rel | log=$log_rel" >> "$project_root/.harness/progress.log"
224
+ say "wake-agent: $project_root (idle ${idle}s, force=${force}, mode=${mode}, executor=${executor}) → $status $log_rel"
225
+
226
+ run_conductor_fallback "$project_root" "$status"
227
+
228
+ if tmux_session="$(find_tmux_session "$project_root" 2>/dev/null)"; then
229
+ tmux display-message -t "$tmux_session" "walwal wake headless tick: $status ($log_rel)" 2>/dev/null || true
230
+ fi
231
+ }
232
+
233
+ PROJECTS_LIST="${HOME}/.walwal-harness/projects.list"
234
+ PROJECTS=()
235
+
236
+ if [ -n "${WALWAL_HARNESS_PROJECTS:-}" ]; then
237
+ IFS=':' read -r -a PROJECTS <<< "$WALWAL_HARNESS_PROJECTS"
238
+ elif [ -f "$PROJECTS_LIST" ]; then
239
+ while IFS= read -r line; do
240
+ line="${line%%#*}"
241
+ line="$(echo "$line" | xargs)"
242
+ [ -z "$line" ] && continue
243
+ PROJECTS+=("$line")
244
+ done < "$PROJECTS_LIST"
245
+ fi
246
+
247
+ if [ "${#PROJECTS[@]}" -eq 0 ]; then
248
+ say "no registered projects (set ~/.walwal-harness/projects.list or WALWAL_HARNESS_PROJECTS)"
249
+ exit 0
250
+ fi
251
+
252
+ # 임계치: idle 60분 이상이면 깨운다 (Stop 훅이 정상 동작하면 거의 연속 활동이므로
253
+ # 1시간 이상 정지 = 어디선가 멈춘 상태로 판단)
254
+ IDLE_THRESHOLD_SECONDS="${HARNESS_WAKE_IDLE_SECONDS:-3300}" # 55분 (안전 마진)
255
+ NOW_EPOCH=$(date "+%s")
256
+ FORCE_WAKE=0
257
+ if in_force_wake_window; then
258
+ FORCE_WAKE=1
259
+ fi
260
+
261
+ for PROJECT_ROOT in "${PROJECTS[@]}"; do
262
+ if [ ! -d "$PROJECT_ROOT" ]; then
263
+ say "skip: $PROJECT_ROOT (not a directory)"
264
+ continue
265
+ fi
266
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
267
+ if [ ! -f "$PROGRESS" ]; then
268
+ say "skip: $PROJECT_ROOT (no .harness/progress.json)"
269
+ continue
270
+ fi
271
+ PROJECT_FORCE_WAKE="$FORCE_WAKE"
272
+
273
+ if command -v jq >/dev/null 2>&1 && [ "$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo idle)" = "waiting_owner" ]; then
274
+ if [ -x "$PROJECT_ROOT/scripts/harness-progress-set.sh" ]; then
275
+ bash "$PROJECT_ROOT/scripts/harness-progress-set.sh" "$PROJECT_ROOT" \
276
+ '.conductor.state = "waiting_meeting" |
277
+ .conductor.current_action = "autonomous-normalize-waiting-owner" |
278
+ .next_agent = "meeting-manager" |
279
+ .agent_status = "pending" |
280
+ .meetings.active = ((.meetings.active // []) + ["meeting-manager"] | unique) |
281
+ .meetings.requested_type = (.meetings.requested_type // "followup-review") |
282
+ .meetings.requested_reason = (.meetings.requested_reason // "autonomous-normalize-waiting-owner") |
283
+ .workflow.stage = "ops-monitoring" |
284
+ .workflow.last_transition = (now | todate) |
285
+ .workflow.last_reason = "autonomous-normalize-waiting-owner"' \
286
+ >/dev/null 2>&1 || true
287
+ say "normalize: $PROJECT_ROOT (conductor=waiting_owner → waiting_meeting)"
288
+ PROJECT_FORCE_WAKE=1
289
+ fi
290
+ fi
291
+
292
+ # idle 시간은 hourly-review 실행 전에 캡처한다. review 는 progress.json 과
293
+ # progress.log 를 갱신하므로, 이후 mtime 을 보면 wake 가 자기 활동을
294
+ # "프로젝트 활동 중"으로 오판한다.
295
+ if MTIME=$(stat_mtime "$PROGRESS"); then :;
296
+ else
297
+ say "skip: $PROJECT_ROOT (cannot stat progress.json)"
298
+ continue
299
+ fi
300
+ IDLE=$((NOW_EPOCH - MTIME))
301
+
302
+ # Every hourly batch must leave disk-backed evidence, even if Claude/tmux is
303
+ # stopped or busy. This deterministic review runs after the idle snapshot.
304
+ if [ -x "$PROJECT_ROOT/scripts/harness-hourly-review.sh" ]; then
305
+ review_output=$(bash "$PROJECT_ROOT/scripts/harness-hourly-review.sh" "$PROJECT_ROOT" 2>/dev/null || true)
306
+ review_path=$(printf "%s\n" "$review_output" | grep -Eo '(/[^[:space:]]+|[.][^[:space:]]+)\.md' | tail -1)
307
+ if [ -z "$review_path" ]; then
308
+ review_path=$(printf "%s\n" "$review_output" | tail -1)
309
+ fi
310
+ if [ -n "$review_path" ]; then
311
+ say "review: $PROJECT_ROOT → $review_path"
312
+ else
313
+ say "review-fail: $PROJECT_ROOT"
314
+ fi
315
+ fi
316
+
317
+ if [ "$IDLE" -lt "$IDLE_THRESHOLD_SECONDS" ] && [ "$PROJECT_FORCE_WAKE" -ne 1 ]; then
318
+ say "skip: $PROJECT_ROOT (idle ${IDLE}s < threshold ${IDLE_THRESHOLD_SECONDS}s — 활동 중)"
319
+ continue
320
+ fi
321
+
322
+ # 회사 루프가 멈춘 상태인지 확인
323
+ if command -v jq >/dev/null 2>&1; then
324
+ CSTATE=$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo "idle")
325
+ SSTATUS=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null || echo "init")
326
+ if [ "$SSTATUS" = "completed" ] || [ "$SSTATUS" = "aborted" ]; then
327
+ say "continue: $PROJECT_ROOT (sprint=$SSTATUS — autonomous review still required)"
328
+ fi
329
+ if [ "$CSTATE" = "paused" ] || [ "$CSTATE" = "completed" ] || [ "$CSTATE" = "escalated" ]; then
330
+ say "continue: $PROJECT_ROOT (conductor=$CSTATE — route through meeting-manager/service-ops)"
331
+ fi
332
+ fi
333
+
334
+ # 발화 직전 service-ops monitor 트리거 partial update.
335
+ # requested_mode 만 set — conductor-tick 이 이걸 보고 service-ops 를 spawn 한다.
336
+ # stream_active 는 service-ops 본인이 spawn 시 §3.1.1 에 따라 직접 set (정직성 룰).
337
+ # auto-retro / incident / null / monitor 가 아닌 모드가 진행 중이면 덮어쓰지 않음.
338
+ if [ -x "$PROJECT_ROOT/scripts/harness-progress-set.sh" ]; then
339
+ bash "$PROJECT_ROOT/scripts/harness-progress-set.sh" "$PROJECT_ROOT" \
340
+ 'if (.service_ops.requested_mode // "") == "" or (.service_ops.requested_mode // "") == "monitor" or (.service_ops.requested_mode // "") == "auto-retro"
341
+ then .service_ops.requested_mode = "monitor" |
342
+ .service_ops.monitor.wake_trigger_at = (now | todate)
343
+ else . end' \
344
+ >/dev/null 2>&1 || true
345
+ fi
346
+
347
+ start_headless_tick "$PROJECT_ROOT" "$IDLE" "$PROJECT_FORCE_WAKE" "$review_path"
348
+ done
349
+
350
+ exit 0
@@ -0,0 +1,188 @@
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
+ source "$SCRIPT_DIR/lib/harness-agent-resolver.sh"
15
+
16
+ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || exit 0
17
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
18
+ CONFIG="$PROJECT_ROOT/.harness/config.json"
19
+ FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
20
+ QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
21
+ WORKER_DIR="$PROJECT_ROOT/.harness/actions/workers"
22
+ LOG_DIR="$PROJECT_ROOT/.harness/ops/workers"
23
+ DISPATCH_LOCK="$PROJECT_ROOT/.harness/.worker-dispatch-lock"
24
+ DISPATCH_LOCK_STALE_SEC="${DISPATCH_LOCK_STALE_SEC:-900}"
25
+
26
+ [ -f "$PROGRESS" ] || exit 0
27
+ [ -f "$CONFIG" ] || exit 0
28
+ [ -f "$FEATURES" ] || exit 0
29
+ [ -x "$SCRIPT_DIR/harness-queue-manager.sh" ] || exit 0
30
+ command -v jq >/dev/null 2>&1 || exit 0
31
+
32
+ mkdir -p "$WORKER_DIR" "$LOG_DIR"
33
+
34
+ acquire_dispatch_lock() {
35
+ if mkdir "$DISPATCH_LOCK" 2>/dev/null; then
36
+ printf '%s\n' "$$" > "$DISPATCH_LOCK/pid"
37
+ printf '%s\n' "$(date +%s)" > "$DISPATCH_LOCK/created_at"
38
+ trap 'rm -rf "$DISPATCH_LOCK" 2>/dev/null || true' EXIT INT TERM
39
+ return 0
40
+ fi
41
+
42
+ local now created age holder
43
+ now="$(date +%s)"
44
+ created="$(cat "$DISPATCH_LOCK/created_at" 2>/dev/null || echo "$now")"
45
+ holder="$(cat "$DISPATCH_LOCK/pid" 2>/dev/null || echo unknown)"
46
+ age=$((now - created))
47
+ if [ "$age" -ge "$DISPATCH_LOCK_STALE_SEC" ]; then
48
+ rm -rf "$DISPATCH_LOCK" 2>/dev/null || true
49
+ if mkdir "$DISPATCH_LOCK" 2>/dev/null; then
50
+ printf '%s\n' "$$" > "$DISPATCH_LOCK/pid"
51
+ printf '%s\n' "$now" > "$DISPATCH_LOCK/created_at"
52
+ trap 'rm -rf "$DISPATCH_LOCK" 2>/dev/null || true' EXIT INT TERM
53
+ return 0
54
+ fi
55
+ fi
56
+
57
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | conductor | worker-dispatch | skipped-locked | holder=${holder} age=${age}s" >> "$PROJECT_ROOT/.harness/progress.log"
58
+ echo "[]"
59
+ exit 0
60
+ }
61
+
62
+ acquire_dispatch_lock
63
+
64
+ spawn_mode="$(jq -r '.company_mode.worker_spawn // "claude"' "$CONFIG" 2>/dev/null || echo claude)"
65
+ pipeline="$(jq -r '.pipeline // "FULLSTACK"' "$PROGRESS" 2>/dev/null || echo FULLSTACK)"
66
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
67
+ stamp="$(date -u +%Y%m%dT%H%M%SZ)"
68
+
69
+ build_prompt() {
70
+ local team="$1" fid="$2" agent="$3" prompt_path="$4" log_path="$5"
71
+ cat > "$prompt_path" <<EOF
72
+ You are running as walwal-harness company worker.
73
+
74
+ Agent: harness-${agent}
75
+ Worker slot: ${team}
76
+ Feature ID: ${fid}
77
+ Project root: ${PROJECT_ROOT}
78
+
79
+ Follow these rules exactly:
80
+ 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}.
81
+ 2. Work only on FEATURE_ID=${fid}. Do not broaden scope.
82
+ 3. Respect IA ownership and do not edit files outside your department.
83
+ 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.
84
+ 5. On success, update feature-list passes and run: bash scripts/harness-queue-manager.sh pass ${fid}
85
+ 6. On failure, write the reason and run: bash scripts/harness-queue-manager.sh fail ${fid} "<short reason>"
86
+ 7. Use partial progress updates only via scripts/harness-progress-set.sh.
87
+ 8. Append real timestamped progress to .harness/progress.log. Never write future timestamps.
88
+
89
+ This worker log is: ${log_path#$PROJECT_ROOT/}
90
+ EOF
91
+ }
92
+
93
+ if [ ! -f "$QUEUE" ]; then
94
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" init all "$PROJECT_ROOT" >/dev/null 2>&1 || exit 0
95
+ else
96
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" recover "$PROJECT_ROOT" >/dev/null 2>&1 || true
97
+ fi
98
+
99
+ pairs="$(bash "$SCRIPT_DIR/harness-queue-manager.sh" auto-dispatch "$PROJECT_ROOT" 2>/dev/null || echo "[]")"
100
+ if ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"$pairs"; then
101
+ pairs="[]"
102
+ fi
103
+
104
+ count="$(jq 'length' <<<"$pairs")"
105
+ if [ "$count" -eq 0 ]; then
106
+ 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 "[]")"
107
+ jq --arg ts "$ts" --argjson workers "$active_workers" '
108
+ .company_state.active_workers = ($workers | length) |
109
+ .company_state.workers = $workers |
110
+ .company_state.last_dispatch_at = (.company_state.last_dispatch_at // null)
111
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
112
+ echo "[]"
113
+ exit 0
114
+ fi
115
+
116
+ dispatches_file="$(mktemp)"
117
+ : > "$dispatches_file"
118
+
119
+ i=0
120
+ while [ "$i" -lt "$count" ]; do
121
+ team="$(jq -r ".[$i].team" <<<"$pairs")"
122
+ fid="$(jq -r ".[$i].feature" <<<"$pairs")"
123
+ phase="$(jq -r --arg fid "$fid" '.queue.in_progress[$fid].phase // "gen"' "$QUEUE" 2>/dev/null || echo gen)"
124
+ agent="$(resolve_feature_agent "$PROJECT_ROOT" "$fid" "$phase" "$pipeline" 2>/dev/null || true)"
125
+ prompt_rel=".harness/actions/workers/T${team}-${fid}-${stamp}.prompt.md"
126
+ log_rel=".harness/ops/workers/T${team}-${fid}-${stamp}.log"
127
+ prompt_path="$PROJECT_ROOT/$prompt_rel"
128
+ log_path="$PROJECT_ROOT/$log_rel"
129
+
130
+ if [ -z "$agent" ]; then
131
+ reason="agent_resolution_failed phase=${phase}"
132
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" fail "$fid" "$PROJECT_ROOT" >/dev/null 2>&1 || true
133
+ printf '%s\n' "$reason" > "$log_path"
134
+ jq -n --argjson team "$team" --arg feature "$fid" --arg phase "$phase" --arg status "blocked" --arg reason "$reason" \
135
+ '{team:$team, feature:$feature, phase:$phase, agent:null, prompt:null, log:null, status:$status, reason:$reason, pid:null}' >> "$dispatches_file"
136
+ echo "$ts | conductor | worker-dispatch | blocked | T${team}/${fid} | ${reason}" >> "$PROJECT_ROOT/.harness/progress.log"
137
+ i=$((i + 1))
138
+ continue
139
+ fi
140
+
141
+ build_prompt "$team" "$fid" "$agent" "$prompt_path" "$log_path"
142
+
143
+ pid="null"
144
+ status="recorded"
145
+ if [ "$spawn_mode" = "claude" ] && command -v claude >/dev/null 2>&1; then
146
+ (
147
+ cd "$PROJECT_ROOT" || exit 1
148
+ claude -p "$(cat "$prompt_path")" > "$log_path" 2>&1
149
+ ) &
150
+ pid="$!"
151
+ status="spawned"
152
+ fi
153
+
154
+ tmpq="${QUEUE}.tmp.$$.$i"
155
+ jq --arg tid "$team" --arg agent "$agent" --arg phase "$phase" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" --arg ts "$ts" '
156
+ .teams[$tid].pid = $pid |
157
+ .teams[$tid].agent = $agent |
158
+ .teams[$tid].phase = $phase |
159
+ .teams[$tid].prompt = $prompt |
160
+ .teams[$tid].log = $log |
161
+ .teams[$tid].spawn_status = $status |
162
+ .teams[$tid].started_at = $ts |
163
+ .queue.in_progress[.teams[$tid].feature].agent = $agent |
164
+ .queue.in_progress[.teams[$tid].feature].phase = $phase |
165
+ .queue.in_progress[.teams[$tid].feature].prompt = $prompt |
166
+ .queue.in_progress[.teams[$tid].feature].log = $log |
167
+ .queue.in_progress[.teams[$tid].feature].pid = $pid
168
+ ' "$QUEUE" > "$tmpq" && mv "$tmpq" "$QUEUE"
169
+
170
+ jq -n --argjson team "$team" --arg feature "$fid" --arg agent "$agent" --arg phase "$phase" --arg prompt "$prompt_rel" --arg log "$log_rel" --arg status "$status" --argjson pid "$pid" \
171
+ '{team:$team, feature:$feature, agent:$agent, phase:$phase, prompt:$prompt, log:$log, status:$status, pid:$pid}' >> "$dispatches_file"
172
+ echo "$ts | conductor | worker-dispatch | $status | T${team}/${fid} | $agent phase=$phase pid=$pid" >> "$PROJECT_ROOT/.harness/progress.log"
173
+ i=$((i + 1))
174
+ done
175
+
176
+ dispatches="$(jq -s '.' "$dispatches_file")"
177
+ rm -f "$dispatches_file"
178
+ 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")"
179
+
180
+ jq --arg ts "$ts" --argjson dispatches "$dispatches" --argjson workers "$active_workers" '
181
+ .company_state.active_workers = ($workers | length) |
182
+ .company_state.workers = $workers |
183
+ .company_state.last_dispatch_at = $ts |
184
+ .company_state.last_dispatch = $dispatches |
185
+ .conductor.worker_dispatches = ((.conductor.worker_dispatches // []) + $dispatches)
186
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
187
+
188
+ echo "$dispatches"
@@ -0,0 +1,127 @@
1
+ #!/bin/bash
2
+ # harness-agent-resolver.sh — canonical feature/phase → harness agent resolver
3
+ #
4
+ # Worker dispatch must not guess the department when Planner already wrote
5
+ # owner_agents/applicable_eval_axes. This resolver is intentionally strict:
6
+ # declared metadata wins; keyword fallback is only allowed when metadata is
7
+ # absent.
8
+
9
+ set -uo pipefail
10
+
11
+ resolve_feature_agent() {
12
+ local project_root="$1" fid="$2" phase="${3:-gen}" pipeline="${4:-FULLSTACK}"
13
+ local features="$project_root/.harness/actions/feature-list.json"
14
+
15
+ [ -f "$features" ] || return 2
16
+ command -v jq >/dev/null 2>&1 || return 2
17
+
18
+ local feature_json
19
+ feature_json="$(jq -c --arg id "$fid" '.features[]? | select(.id == $id)' "$features" 2>/dev/null | head -1)"
20
+ [ -n "$feature_json" ] && [ "$feature_json" != "null" ] || return 3
21
+
22
+ case "$phase" in
23
+ eval|eval:*|evaluation|evaluator-*)
24
+ _resolve_eval_agent "$feature_json" "$phase"
25
+ return $?
26
+ ;;
27
+ *)
28
+ _resolve_generation_agent "$feature_json" "$pipeline"
29
+ return $?
30
+ ;;
31
+ esac
32
+ }
33
+
34
+ _resolve_generation_agent() {
35
+ local feature_json="$1" pipeline="$2"
36
+ local owner_count agent
37
+
38
+ owner_count="$(jq '(.owner_agents // []) | length' <<<"$feature_json")"
39
+ if [ "${owner_count:-0}" -gt 0 ]; then
40
+ agent="$(jq -r '
41
+ (.owner_agents // [])
42
+ | map(select(
43
+ . == "generator-backend" or
44
+ . == "generator-frontend" or
45
+ . == "generator-designer" or
46
+ . == "generator-devops"
47
+ ))
48
+ | .[0] // empty
49
+ ' <<<"$feature_json")"
50
+ [ -n "$agent" ] || return 4
51
+ echo "$agent"
52
+ return 0
53
+ fi
54
+
55
+ local layer service title text
56
+ layer="$(jq -r '(.layer // .type // "")' <<<"$feature_json")"
57
+ service="$(jq -r '(.service // "")' <<<"$feature_json")"
58
+ title="$(jq -r '(.title // .name // .description // "")' <<<"$feature_json")"
59
+ text="$(printf '%s %s %s' "$layer" "$service" "$title" | tr '[:upper:]' '[:lower:]')"
60
+
61
+ case "$text" in
62
+ *frontend*|*ui*|*web*|*react*|*next*) echo "generator-frontend" ;;
63
+ *design*|*token*|*component-spec*) echo "generator-designer" ;;
64
+ *devops*|*infra*|*deploy*|*ci*) echo "generator-devops" ;;
65
+ *)
66
+ if [ "$pipeline" = "FE-ONLY" ]; then echo "generator-frontend"; else echo "generator-backend"; fi
67
+ ;;
68
+ esac
69
+ }
70
+
71
+ _resolve_eval_agent() {
72
+ local feature_json="$1" phase="$2"
73
+ local requested_axis axes_count agent
74
+
75
+ requested_axis="${phase#eval:}"
76
+ requested_axis="${requested_axis#evaluator-}"
77
+ [ "$requested_axis" = "$phase" ] && requested_axis=""
78
+ [ "$requested_axis" = "eval" ] && requested_axis=""
79
+ [ "$requested_axis" = "evaluation" ] && requested_axis=""
80
+
81
+ axes_count="$(jq '(.applicable_eval_axes // []) | length' <<<"$feature_json")"
82
+ [ "${axes_count:-0}" -gt 0 ] || return 5
83
+
84
+ if [ -n "$requested_axis" ]; then
85
+ agent="$(_axis_to_evaluator "$requested_axis")" || return 6
86
+ jq -e --arg agent "$agent" '
87
+ def map_axis:
88
+ if . == "code-quality" or . == "code_quality" or . == "evaluator-code-quality" then "evaluator-code-quality"
89
+ elif . == "functional" or . == "evaluator-functional" then "evaluator-functional"
90
+ elif . == "visual" or . == "evaluator-visual" then "evaluator-visual"
91
+ elif . == "architecture" or . == "evaluator-architecture" then "evaluator-architecture"
92
+ elif . == "security" or . == "evaluator-security" then "evaluator-security"
93
+ else empty end;
94
+ (.applicable_eval_axes // []) | map(map_axis) | index($agent)
95
+ ' <<<"$feature_json" >/dev/null || return 7
96
+ echo "$agent"
97
+ return 0
98
+ fi
99
+
100
+ agent="$(jq -r '
101
+ def map_axis:
102
+ if . == "code-quality" or . == "code_quality" or . == "evaluator-code-quality" then "evaluator-code-quality"
103
+ elif . == "functional" or . == "evaluator-functional" then "evaluator-functional"
104
+ elif . == "visual" or . == "evaluator-visual" then "evaluator-visual"
105
+ elif . == "architecture" or . == "evaluator-architecture" then "evaluator-architecture"
106
+ elif . == "security" or . == "evaluator-security" then "evaluator-security"
107
+ else empty end;
108
+ (.applicable_eval_axes // []) | map(map_axis) | .[0] // empty
109
+ ' <<<"$feature_json")"
110
+ [ -n "$agent" ] || return 6
111
+ echo "$agent"
112
+ }
113
+
114
+ _axis_to_evaluator() {
115
+ case "$1" in
116
+ code-quality|code_quality|evaluator-code-quality) echo "evaluator-code-quality" ;;
117
+ functional|evaluator-functional) echo "evaluator-functional" ;;
118
+ visual|evaluator-visual) echo "evaluator-visual" ;;
119
+ architecture|evaluator-architecture) echo "evaluator-architecture" ;;
120
+ security|evaluator-security) echo "evaluator-security" ;;
121
+ *) return 1 ;;
122
+ esac
123
+ }
124
+
125
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
126
+ resolve_feature_agent "${1:-.}" "${2:-}" "${3:-gen}" "${4:-FULLSTACK}"
127
+ fi