@walwal-harness/cli 6.1.4 → 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 (55) hide show
  1. package/CHANGELOG.md +28 -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/config.json +25 -27
  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/shared.md +16 -0
  11. package/gotchas/conductor.md +16 -0
  12. package/gotchas/dispatcher.md +16 -0
  13. package/gotchas/meeting-manager.md +8 -0
  14. package/gotchas/service-ops.md +8 -0
  15. package/package.json +4 -4
  16. package/scripts/conductor-tick.sh +61 -53
  17. package/scripts/harness-archive.sh +7 -5
  18. package/scripts/harness-dashboard-up.sh +11 -3
  19. package/scripts/harness-hourly-review.sh +303 -0
  20. package/scripts/harness-meeting-doc.sh +55 -16
  21. package/scripts/harness-next.sh +1 -1
  22. package/scripts/harness-queue-manager.sh +12 -5
  23. package/scripts/harness-service-ops-monitor.sh +228 -0
  24. package/scripts/harness-session-start.sh +60 -31
  25. package/scripts/harness-statusline.sh +8 -12
  26. package/scripts/harness-stop.sh +85 -0
  27. package/scripts/harness-user-prompt-submit.sh +11 -29
  28. package/scripts/harness-wake-install.sh +179 -0
  29. package/scripts/harness-wake.sh +258 -0
  30. package/scripts/harness-worker-dispatch.sh +157 -0
  31. package/scripts/lib/harness-progress-migrate.sh +6 -1
  32. package/scripts/lib/harness-render-progress.sh +3 -3
  33. package/skills/brainstorming/SKILL.md +2 -2
  34. package/skills/conductor/SKILL.md +67 -55
  35. package/skills/cqo/SKILL.md +1 -0
  36. package/skills/cto/SKILL.md +1 -0
  37. package/skills/dispatcher/SKILL.md +33 -14
  38. package/skills/evaluator-code-quality/SKILL.md +4 -4
  39. package/skills/evaluator-functional/SKILL.md +6 -6
  40. package/skills/evaluator-visual/SKILL.md +4 -4
  41. package/skills/generator-backend/SKILL.md +4 -4
  42. package/skills/generator-frontend/SKILL.md +5 -5
  43. package/skills/meeting-manager/SKILL.md +82 -12
  44. package/skills/planner/SKILL.md +3 -3
  45. package/skills/service-ops/SKILL.md +25 -0
  46. package/commands/harness-solo.md +0 -103
  47. package/commands/harness-stop.md +0 -53
  48. package/commands/harness-team.md +0 -530
  49. package/scripts/harness-dashboard.sh +0 -509
  50. package/scripts/harness-goal-init.sh +0 -72
  51. package/scripts/harness-goal-show.sh +0 -37
  52. package/scripts/harness-gotcha-memory.sh +0 -348
  53. package/scripts/harness-monitor.sh +0 -398
  54. package/scripts/harness-prompt-history.sh +0 -164
  55. package/scripts/harness-tmux.sh +0 -372
@@ -9,6 +9,7 @@ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || exit 0
9
9
  PROGRESS="$PROJECT_ROOT/.harness/progress.json"
10
10
  CONFIG="$PROJECT_ROOT/.harness/config.json"
11
11
  FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
12
+ QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
12
13
 
13
14
  [ -f "$PROGRESS" ] || exit 0
14
15
  [ -f "$CONFIG" ] || exit 0
@@ -22,12 +23,11 @@ fi
22
23
  current_agent=$(jq -r '.current_agent // "null"' "$PROGRESS")
23
24
  agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
24
25
  pipeline=$(jq -r '.pipeline // "null"' "$PROGRESS")
25
- mode=$(jq -r '.mode // "auto"' "$PROGRESS")
26
+ mode=$(jq -r '.mode // "company"' "$PROGRESS")
26
27
  # v6.2 — parallel tracks (fork-join) state
27
28
  parallel_tracks=$(jq -c '.conductor.tracks // []' "$PROGRESS")
28
29
  parallel_rendezvous=$(jq -c '.conductor.rendezvous // null' "$PROGRESS")
29
30
  parallel_pending_count=$(jq -r '[ .conductor.tracks // [] | .[] | select(.status == "pending" or .status == "running") ] | length' "$PROGRESS")
30
- user_override=$(jq -r '.mode_decision.user_override // "null"' "$PROGRESS")
31
31
  requested_mode=$(jq -r '.service_ops.requested_mode // "null"' "$PROGRESS")
32
32
  planner_requested_mode=$(jq -r '.planner.requested_mode // "null"' "$PROGRESS")
33
33
  planner_last_brief=$(jq -r '.planner.last_brief // "null"' "$PROGRESS")
@@ -57,50 +57,16 @@ escape_json_string() {
57
57
  jq -Rn --arg v "$1" '$v'
58
58
  }
59
59
 
60
- maybe_select_mode() {
61
- local effective="$mode"
62
- if [ "$user_override" != "null" ] && [ -n "$user_override" ]; then
63
- effective="$user_override"
64
- set_progress ".mode = \"$effective\" |
65
- .mode_decision.owner = \"conductor\" |
66
- .mode_decision.user_override = \"$effective\" |
60
+ normalize_company_mode() {
61
+ if [ "$mode" != "company" ]; then
62
+ set_progress '.mode = "company" |
63
+ .mode_decision.owner = "conductor" |
64
+ .mode_decision.policy = "always_company_parallel" |
65
+ .mode_decision.user_override = null |
67
66
  .mode_decision.decided_at = (now | todate) |
68
- .mode_decision.rationale = \"user override: $effective\""
69
- echo "$effective"
70
- return
71
- fi
72
- if [ "$mode" != "auto" ] || [ ! -f "$FEATURES" ]; then
73
- echo "$mode"
74
- return
67
+ .mode_decision.rationale = "legacy mode normalized to always-on company mode"'
75
68
  fi
76
-
77
- local feature_count ready_at_start critical_depth
78
- feature_count=$(jq '.features | length // 0' "$FEATURES" 2>/dev/null || echo 0)
79
- ready_at_start=$(jq '[.features[]? | select(((.depends_on // []) | length) == 0)] | length' "$FEATURES" 2>/dev/null || echo 0)
80
- critical_depth=$(jq '
81
- def feature($id): .features[]? | select(.id == $id);
82
- def depth($id):
83
- (feature($id) | .depends_on // []) as $deps
84
- | if ($deps | length) == 0 then 1 else ([ $deps[] | depth(.) ] | max + 1) end;
85
- ([.features[]?.id | depth(.)] | max) // 0
86
- ' "$FEATURES" 2>/dev/null || echo 0)
87
-
88
- local team_ready team_features team_depth selected rationale
89
- team_ready=$(jq -r '.mode_selection.rules.force_team_when.ready_at_start_gte // 3' "$CONFIG")
90
- team_features=$(jq -r '.mode_selection.rules.force_team_when.feature_count_gte // 6' "$CONFIG")
91
- team_depth=$(jq -r '.mode_selection.rules.force_team_when.critical_path_depth_lte // 2' "$CONFIG")
92
-
93
- if [ "$ready_at_start" -ge "$team_ready" ] && [ "$feature_count" -ge "$team_features" ] && [ "$critical_depth" -le "$team_depth" ]; then
94
- selected="team"
95
- else
96
- selected=$(jq -r '.mode_selection.rules.tie_breaker // "team"' "$CONFIG")
97
- fi
98
- rationale="company-default: ready=${ready_at_start}, features=${feature_count}, depth=${critical_depth} -> ${selected}"
99
- set_progress ".mode = \"$selected\" |
100
- .mode_decision.owner = \"conductor\" |
101
- .mode_decision.decided_at = (now | todate) |
102
- .mode_decision.rationale = \"$rationale\""
103
- echo "$selected"
69
+ echo "company"
104
70
  }
105
71
 
106
72
  infer_drift_classification() {
@@ -140,6 +106,18 @@ meeting_stage_for_owner() {
140
106
  esac
141
107
  }
142
108
 
109
+ ensure_team_queue() {
110
+ [ "$effective_mode" = "company" ] || return 0
111
+ [ -f "$FEATURES" ] || return 0
112
+ [ -x "$SCRIPT_DIR/harness-queue-manager.sh" ] || return 0
113
+
114
+ if [ ! -f "$QUEUE" ]; then
115
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" init all "$PROJECT_ROOT" >/dev/null 2>&1 || true
116
+ else
117
+ bash "$SCRIPT_DIR/harness-queue-manager.sh" recover "$PROJECT_ROOT" >/dev/null 2>&1 || true
118
+ fi
119
+ }
120
+
143
121
  load_meeting_decision() {
144
122
  local decision
145
123
  decision=$(bash "$SCRIPT_DIR/harness-meeting-doc.sh" "$PROJECT_ROOT" read-decision 2>/dev/null || true)
@@ -150,7 +128,15 @@ load_meeting_decision() {
150
128
  fi
151
129
  }
152
130
 
153
- effective_mode="$(maybe_select_mode)"
131
+ dispatch_company_workers() {
132
+ [ "$effective_mode" = "company" ] || { echo "[]"; return 0; }
133
+ [ -f "$FEATURES" ] || { echo "[]"; return 0; }
134
+ [ -x "$SCRIPT_DIR/harness-worker-dispatch.sh" ] || { echo "[]"; return 0; }
135
+ bash "$SCRIPT_DIR/harness-worker-dispatch.sh" "$PROJECT_ROOT" 2>/dev/null || echo "[]"
136
+ }
137
+
138
+ effective_mode="$(normalize_company_mode)"
139
+ ensure_team_queue
154
140
  drift_classification="$(infer_drift_classification)"
155
141
 
156
142
  next="$candidate"
@@ -241,6 +227,10 @@ elif [ "$parallel_pending_count" -gt 0 ] && [ "$agent_status" = "completed" ] &&
241
227
  planner_filter=".planner.requested_mode = \"$next_action\""
242
228
  fi
243
229
  meeting_filter=".conductor.tracks = $updated_tracks | .meetings.decision.tracks = $updated_tracks"
230
+ else
231
+ next="$candidate"
232
+ action="parallel:await-rendezvous"
233
+ meeting_filter=".conductor.tracks = $updated_tracks | .meetings.decision.tracks = $updated_tracks"
244
234
  fi
245
235
  else
246
236
  # All tracks completed → rendezvous (followup-review)
@@ -311,23 +301,26 @@ elif [ "$current_agent" = "meeting-manager" ] && [ "$agent_status" = "completed"
311
301
  implementation_drift) owner="cto" ;;
312
302
  planning_drift) owner="planner" ;;
313
303
  ops_drift) owner="service-ops" ;;
314
- goal_drift) owner="dispatcher" ;;
304
+ goal_drift) owner="planner" ;;
315
305
  *)
316
306
  if [ "$plan_artifacts_ready" = false ]; then owner="planner"; else owner="cto"; fi
317
307
  ;;
318
308
  esac
319
309
  fi
320
310
 
321
- # v6.2 — Parallel fork: if 2+ tracks, materialize conductor.tracks[] and dispatch
322
- # the first pending track owner. Subsequent ticks advance remaining tracks.
311
+ # v6.2 — Parallel fork: if 2+ tracks, materialize all tracks as running.
312
+ # `next_agent` keeps the first owner only for backward-compatible handoff display;
313
+ # the fork state itself must expose every track as active in the same tick.
323
314
  if [ "$(jq 'length' <<<"$decision_tracks")" -gt 1 ]; then
324
315
  first_track_owner=$(jq -r '.[0].owner' <<<"$decision_tracks")
325
316
  first_track_action=$(jq -r '.[0].action_type // "triage"' <<<"$decision_tracks")
326
317
  first_track_id=$(jq -r '.[0].id // "track-1"' <<<"$decision_tracks")
327
- # mark first track as running, rest as pending
328
- materialized_tracks=$(jq -c --arg first "$first_track_id" '
318
+ active_track_owners=$(jq -c '[.[] | .owner] | unique' <<<"$decision_tracks")
319
+ # mark every independent track as running; rendezvous waits for completion.
320
+ materialized_tracks=$(jq -c '
329
321
  [ .[] | . + {
330
- status: (if .id == $first then "running" else (.status // "pending") end),
322
+ status: "running",
323
+ started_at: (now | todate),
331
324
  deliverable_path: (.deliverable_path // null)
332
325
  }
333
326
  ]
@@ -347,6 +340,7 @@ elif [ "$current_agent" = "meeting-manager" ] && [ "$agent_status" = "completed"
347
340
  .meetings.requested_reason = null |
348
341
  .meetings.requested_tracks = [] |
349
342
  .meetings.requested_rendezvous = null |
343
+ .meetings.active = $active_track_owners |
350
344
  .meetings.decision = {
351
345
  \"owner\": \"$owner\",
352
346
  \"action_type\": \"$action_type\",
@@ -502,13 +496,13 @@ elif [ "$candidate" = "archive" ]; then
502
496
  local_drift="$drift_classification"
503
497
  if [ "$requested_mode" = "auto-retro" ] || [ "${open_incidents:-0}" -gt 0 ] || [ "${ops_alerts:-0}" -gt 0 ] || { [ "$goal_adherence" != "null" ] && awk "BEGIN {exit !($goal_adherence < 0.7)}"; }; then
504
498
  next="meeting-manager"
505
- action="convene:sprint-review:${local_drift}"
499
+ action="convene:operating-review:${local_drift}"
506
500
  new_conductor_state="waiting_meeting"
507
501
  new_workflow_stage="ops-review"
508
502
  meeting_prepare=true
509
503
  meeting_filter="
510
504
  .meetings.active = [\"meeting-manager\"] |
511
- .meetings.requested_type = \"sprint-review\" |
505
+ .meetings.requested_type = \"followup-review\" |
512
506
  .meetings.requested_reason = \"ops-batch\" |
513
507
  .service_ops.drift_classification = \"$local_drift\" |
514
508
  .meetings.decision = {
@@ -553,6 +547,20 @@ if [ "$next" = "meeting-manager" ] && [ "$workflow_stage" = "ops-review" ]; then
553
547
  filter="$filter | .workflow.loop_count = ((.workflow.loop_count // 0) + 1)"
554
548
  fi
555
549
 
550
+ worker_dispatches="[]"
551
+ if [ "$new_conductor_state" = "running" ] && [ "$next" != "meeting-manager" ] && [ "$next" != "service-ops" ] && [ "$next" != "archive" ]; then
552
+ worker_dispatches="$(dispatch_company_workers)"
553
+ if jq -e 'type == "array" and length > 0' >/dev/null 2>&1 <<<"$worker_dispatches"; then
554
+ dispatch_count="$(jq 'length' <<<"$worker_dispatches")"
555
+ filter="$filter |
556
+ .conductor.current_action = \"dispatch:workers:${dispatch_count}\" |
557
+ .next_agent = \"conductor\" |
558
+ .workflow.stage = \"implementation\" |
559
+ .workflow.last_reason = \"dispatch:workers:${dispatch_count}\""
560
+ next="conductor"
561
+ fi
562
+ fi
563
+
556
564
  set_progress "$filter"
557
565
 
558
566
  if [ "$meeting_prepare" = true ] && [ "$next" = "meeting-manager" ]; then
@@ -88,15 +88,17 @@ jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
88
88
  | .current_agent = null
89
89
  | .agent_status = "pending"
90
90
  | .completed_agents = []
91
- | .next_agent = "dispatcher"
92
- | .workflow = { stage: "goal-intake", last_transition: null, loop_count: 0, last_reason: null }
91
+ | .next_agent = "meeting-manager"
92
+ | .conductor.state = "waiting_meeting"
93
+ | .conductor.current_action = "autonomous-post-archive-review"
94
+ | .workflow = { stage: "ops-monitoring", last_transition: $now, loop_count: 0, last_reason: "autonomous-post-archive-review" }
93
95
  | .task_sessions.current = { agent: null, id: null, path: null, source: null }
94
96
  | .failure = { agent: null, location: null, message: null, retry_target: null }
95
- | .meetings.active = []
97
+ | .meetings.active = ["meeting-manager"]
98
+ | .meetings.requested_type = "followup-review"
99
+ | .meetings.requested_reason = "autonomous-post-archive"
96
100
  | .meetings.current_id = null
97
101
  | .meetings.current_record_path = null
98
- | .meetings.requested_type = null
99
- | .meetings.requested_reason = null
100
102
  | .meetings.last_type = null
101
103
  | .meetings.last_reason = null
102
104
  | .meetings.last_decision = null
@@ -4,7 +4,7 @@
4
4
  # 처음 쓰는 사용자도 한 줄로 대시보드를 띄워볼 수 있게 만든 helper.
5
5
  # walwal-harness CLI 패키지에는 R3F + Next.js 무게 때문에 dashboard 가 미포함이고,
6
6
  # 본 스크립트가 git sparse-checkout 으로 apps/harness-dashboard/ 만 가져와서
7
- # 사용자 프로젝트 외부 (~/.walwal-harness/dashboard) 에 격리 설치 후 dev 실행한다.
7
+ # 사용자 프로젝트 외부 (~/.walwal-harness/dashboard/<project-key>) 에 격리 설치 후 dev 실행한다.
8
8
  #
9
9
  # Usage:
10
10
  # bash scripts/harness-dashboard-up.sh # install + dev (port 3001)
@@ -14,13 +14,13 @@ set -euo pipefail
14
14
 
15
15
  REPO_URL="${WALWAL_HARNESS_REPO:-https://github.com/30eggis/walwal-harness.git}"
16
16
  DASHBOARD_PATH="apps/harness-dashboard"
17
- LOCAL_DIR="${HOME}/.walwal-harness/dashboard"
18
17
  PORT="${PORT:-3001}"
18
+ REINSTALL=false
19
19
 
20
20
  while [[ $# -gt 0 ]]; do
21
21
  case "$1" in
22
22
  --port) PORT="$2"; shift 2 ;;
23
- --reinstall) rm -rf "$LOCAL_DIR"; shift ;;
23
+ --reinstall) REINSTALL=true; shift ;;
24
24
  --help|-h)
25
25
  sed -n '2,16p' "$0"
26
26
  exit 0 ;;
@@ -37,6 +37,14 @@ if [[ ! -d "${HARNESS_ROOT}/.harness" ]]; then
37
37
  exit 3
38
38
  fi
39
39
 
40
+ PROJECT_KEY="$(printf '%s' "${HARNESS_ROOT}" | shasum -a 1 | awk '{print substr($1,1,12)}')"
41
+ PROJECT_NAME="$(basename "${HARNESS_ROOT}")"
42
+ LOCAL_DIR="${HOME}/.walwal-harness/dashboard/${PROJECT_NAME}-${PROJECT_KEY}"
43
+
44
+ if [[ "$REINSTALL" == "true" ]]; then
45
+ rm -rf "$LOCAL_DIR"
46
+ fi
47
+
40
48
  echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
41
49
  echo "[brick-office] dashboard 격리 경로 = ${LOCAL_DIR}"
42
50
 
@@ -0,0 +1,303 @@
1
+ #!/bin/bash
2
+ # harness-hourly-review.sh — deterministic hourly executive meeting record
3
+ #
4
+ # Every hourly batch must leave evidence. This writes executive meeting minutes
5
+ # and, for production services, runs Service-Ops monitoring first.
6
+
7
+ set -uo pipefail
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
11
+
12
+ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || exit 0
13
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
14
+ CONFIG="$PROJECT_ROOT/.harness/config.json"
15
+
16
+ [ -f "$PROGRESS" ] || exit 0
17
+ [ -f "$CONFIG" ] || exit 0
18
+ command -v jq >/dev/null 2>&1 || exit 0
19
+
20
+ if [ "$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo idle)" = "waiting_owner" ]; then
21
+ jq '
22
+ .conductor.state = "waiting_meeting" |
23
+ .conductor.current_action = "autonomous-normalize-waiting-owner" |
24
+ .next_agent = "meeting-manager" |
25
+ .agent_status = "pending" |
26
+ .meetings.active = ((.meetings.active // []) + ["meeting-manager"] | unique) |
27
+ .meetings.requested_type = (.meetings.requested_type // "followup-review") |
28
+ .meetings.requested_reason = (.meetings.requested_reason // "autonomous-normalize-waiting-owner") |
29
+ .workflow.stage = "ops-monitoring" |
30
+ .workflow.last_transition = (now | todate) |
31
+ .workflow.last_reason = "autonomous-normalize-waiting-owner"
32
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
33
+ fi
34
+
35
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
36
+ id="M-$(date -u +%Y%m%dT%H%M%SZ)-hourly"
37
+ meeting_dir="$PROJECT_ROOT/.harness/actions/meetings/$id"
38
+ meeting_rel=".harness/actions/meetings/$id/meeting-$id.md"
39
+ meeting_path="$PROJECT_ROOT/$meeting_rel"
40
+ log_path="$PROJECT_ROOT/.harness/progress.log"
41
+
42
+ mkdir -p "$meeting_dir"
43
+
44
+ ops_report=""
45
+ if [ -x "$SCRIPT_DIR/harness-service-ops-monitor.sh" ]; then
46
+ ops_report="$(bash "$SCRIPT_DIR/harness-service-ops-monitor.sh" "$PROJECT_ROOT" 2>/dev/null || true)"
47
+ fi
48
+
49
+ project_name="$(basename "$PROJECT_ROOT")"
50
+ goal_id="$(jq -r '.goals.active_id // "none"' "$PROGRESS" 2>/dev/null || echo none)"
51
+ current_agent="$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null || echo none)"
52
+ next_agent="$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null || echo none)"
53
+ agent_status="$(jq -r '.agent_status // "unknown"' "$PROGRESS" 2>/dev/null || echo unknown)"
54
+ conductor_state="$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo idle)"
55
+ tick_count="$(jq -r '.conductor.tick_count // 0' "$PROGRESS" 2>/dev/null || echo 0)"
56
+ health_count="$(jq -r '(.service_ops.health // []) | length' "$PROGRESS" 2>/dev/null || echo 0)"
57
+ alert_count="$(jq -r '[.service_ops.health[]? | select(.status == "down" or .status == "degraded")] | length' "$PROGRESS" 2>/dev/null || echo 0)"
58
+ warn_count="$(jq -r '[.service_ops.health[]? | select(.status == "warn" or .status == "log_missing")] | length' "$PROGRESS" 2>/dev/null || echo 0)"
59
+ queue_summary="not configured"
60
+ if [ -f "$PROJECT_ROOT/.harness/actions/feature-queue.json" ]; then
61
+ queue_summary="$(jq -r '"ready=\(.queue.ready|length), in_progress=\(.queue.in_progress|length), passed=\(.queue.passed|length), failed=\(.queue.failed|length)"' "$PROJECT_ROOT/.harness/actions/feature-queue.json" 2>/dev/null || echo "unreadable")"
62
+ fi
63
+ drift="$(jq -r '.service_ops.drift_classification // .meetings.decision.drift_classification // "unknown"' "$PROGRESS" 2>/dev/null || echo unknown)"
64
+ last_decision="$(jq -r '.meetings.last_decision // "none"' "$PROGRESS" 2>/dev/null || echo none)"
65
+ last_meeting="$(jq -r '.meetings.last_review_path // .meetings.current_record_path // "none"' "$PROGRESS" 2>/dev/null || echo none)"
66
+ cqo_audit="$(jq -r '.cqo.audit_path // empty' "$PROGRESS" 2>/dev/null || true)"
67
+ cto_review="$(jq -r '.cto.review_path // empty' "$PROGRESS" 2>/dev/null || true)"
68
+ open_incidents="$(jq -r '(.service_ops.incident.open // []) | length' "$PROGRESS" 2>/dev/null || echo 0)"
69
+ owner_escalation="false"
70
+
71
+ verdict="working"
72
+ if [ "$alert_count" -gt 0 ]; then
73
+ verdict="incident"
74
+ elif [ "$conductor_state" = "escalated" ]; then
75
+ verdict="owner_needed"
76
+ elif [ "$conductor_state" = "waiting_owner" ]; then
77
+ verdict="autonomous_review"
78
+ elif [ "$current_agent" = "none" ] && [ "$next_agent" = "none" ]; then
79
+ verdict="idle"
80
+ fi
81
+
82
+ meeting_type="standup"
83
+ decision_owner="planner"
84
+ decision_action="execution-plan"
85
+ decision_rationale="정기 회의에서 active GOAL 대비 현재 작업과 운영 신호를 재정렬하고 다음 work package를 명확히 한다."
86
+ case "$next_agent" in
87
+ planner|cto|cqo|service-ops|dispatcher)
88
+ decision_owner="$next_agent"
89
+ decision_action="continue-current-handoff"
90
+ decision_rationale="정기 회의에서 현재 handoff가 GOAL과 충돌하지 않음을 확인했으므로 예정된 내부 owner가 계속 진행한다."
91
+ ;;
92
+ esac
93
+ if [ "$verdict" = "incident" ]; then
94
+ meeting_type="incident-war-room"
95
+ decision_owner="cto"
96
+ decision_action="runtime-recovery"
97
+ decision_rationale="Service-Ops가 down/degraded 신호를 발견했으므로 CTO가 복구 경로를 정하고 CQO가 복구 evidence를 검증한다."
98
+ elif [ "$verdict" = "idle" ] || [ "$verdict" = "autonomous_review" ]; then
99
+ meeting_type="followup-review"
100
+ decision_owner="planner"
101
+ decision_action="goal-alignment"
102
+ decision_rationale="Owner 대기 없이 CEO/COO/CTO/CQO/Service-Ops가 GOAL 대비 다음 work package를 재정렬한다."
103
+ elif [ "$verdict" = "owner_needed" ]; then
104
+ meeting_type="all-hands"
105
+ decision_owner="dispatcher"
106
+ decision_action="escalate-owner"
107
+ decision_rationale="conductor가 escalated 상태이므로 CEO가 Owner escalation 필요성을 판정한다."
108
+ owner_escalation="true"
109
+ fi
110
+
111
+ decision_json="$(jq -n \
112
+ --arg owner "$decision_owner" \
113
+ --arg action_type "$decision_action" \
114
+ --arg rationale "$decision_rationale" \
115
+ --arg drift "$drift" \
116
+ --arg source "$meeting_rel" \
117
+ --arg ops_report "$ops_report" \
118
+ --arg cqo_audit "$cqo_audit" \
119
+ --arg cto_review "$cto_review" \
120
+ --argjson owner_escalation "$owner_escalation" '
121
+ {
122
+ owner: $owner,
123
+ action_type: $action_type,
124
+ rationale: $rationale,
125
+ evidence: (
126
+ [
127
+ (if $ops_report != "" then {source:$ops_report, kind:"ops-report"} else empty end),
128
+ (if $cqo_audit != "" then {source:$cqo_audit, kind:"cqo-audit"} else empty end),
129
+ (if $cto_review != "" then {source:$cto_review, kind:"cto-review"} else empty end)
130
+ ]
131
+ ),
132
+ drift_classification: $drift,
133
+ source_path: $source,
134
+ owner_escalation: $owner_escalation,
135
+ tracks: [],
136
+ rendezvous: null
137
+ }')"
138
+ meeting_json="$(jq -n \
139
+ --arg meeting_id "$id" \
140
+ --arg type "$meeting_type" \
141
+ --arg reason "$verdict" \
142
+ --arg source_path "$meeting_rel" \
143
+ --argjson decision "$decision_json" \
144
+ '{meeting_id:$meeting_id,type:$type,reason:$reason,decision:($decision + {source_path:$source_path})}')"
145
+
146
+ {
147
+ echo "# Hourly Executive Meeting Minutes"
148
+ echo ""
149
+ echo "- id: $id"
150
+ echo "- ts: $ts"
151
+ echo "- project: $project_name"
152
+ echo "- goal: $goal_id"
153
+ echo "- type: $meeting_type"
154
+ echo "- verdict: $verdict"
155
+ echo "- chair: Meeting-Manager"
156
+ echo "- attendees: Dispatcher/CEO, Planner/COO, CTO, CQO, Service-Ops"
157
+ echo ""
158
+ echo "## Evidence Read Before Discussion"
159
+ echo ""
160
+ echo "- conductor: $conductor_state"
161
+ echo "- tick_count: $tick_count"
162
+ echo "- current_agent: $current_agent ($agent_status)"
163
+ echo "- next_agent: $next_agent"
164
+ echo "- queue: $queue_summary"
165
+ echo "- drift_classification: $drift"
166
+ echo "- previous_decision: $last_decision"
167
+ echo "- previous_meeting: $last_meeting"
168
+ if [ -n "$ops_report" ]; then
169
+ echo "- service_ops_report: $ops_report"
170
+ fi
171
+ if [ -n "$cto_review" ]; then
172
+ echo "- cto_review: $cto_review"
173
+ fi
174
+ if [ -n "$cqo_audit" ]; then
175
+ echo "- cqo_audit: $cqo_audit"
176
+ fi
177
+ echo ""
178
+ echo "## Role Briefs"
179
+ echo ""
180
+ echo "### Dispatcher/CEO"
181
+ case "$verdict" in
182
+ owner_needed)
183
+ echo "- 판단: escalation 상태이므로 Owner에게 외부 결정이 필요한지 확인한다."
184
+ ;;
185
+ *)
186
+ echo "- 판단: 최초 GOAL 이후 Owner 입력은 interrupt다. 회사는 Owner를 기다리지 않고 GOAL 달성 루프를 계속한다."
187
+ ;;
188
+ esac
189
+ echo "- 관심사: active GOAL, KPI, Owner-facing escalation 필요성."
190
+ echo ""
191
+ echo "### Planner/COO"
192
+ if [ "$verdict" = "idle" ] || [ "$verdict" = "autonomous_review" ]; then
193
+ echo "- 판단: work package가 멈췄거나 Owner 대기처럼 보이는 상태다. 다음 operating cycle을 재정렬해야 한다."
194
+ else
195
+ echo "- 판단: 현재 queue와 next_agent가 GOAL에 맞는지 유지 점검한다."
196
+ fi
197
+ echo "- 관심사: planning drift, hypothesis 필요성, 다음 work package 정의."
198
+ echo ""
199
+ echo "### CTO"
200
+ if [ "$alert_count" -gt 0 ]; then
201
+ echo "- 판단: 운영 장애가 구현/런타임 복구 대상인지 우선 판정하고 복구 owner를 지정해야 한다."
202
+ else
203
+ echo "- 판단: 현재 구현/아키텍처 흐름이 다음 handoff를 받을 수 있는지 확인한다."
204
+ fi
205
+ echo "- 관심사: implementation drift, runtime recovery, generator/evaluator 재배정."
206
+ echo ""
207
+ echo "### CQO"
208
+ echo "- 판단: 회의 결론은 evidence가 있는 항목만 통과시킨다. 복구/변경 후 회귀 검증 기준을 요구한다."
209
+ echo "- 관심사: quality drift, missing tests, PASS/FAIL 근거."
210
+ echo ""
211
+ echo "### Service-Ops"
212
+ echo ""
213
+ echo "- monitored_services: $health_count"
214
+ echo "- warnings: $warn_count"
215
+ echo "- alerts: $alert_count"
216
+ echo "- open_incidents: $open_incidents"
217
+ echo "- 판단: 운영 신호를 회의에 제출하고, 장애 시 incident-war-room을 요구한다."
218
+ echo ""
219
+ if [ "$health_count" -gt 0 ]; then
220
+ echo "| Service | Port | Health | Log | Status |"
221
+ echo "|---|---:|---|---|---|"
222
+ jq -r '(.service_ops.health // [])[] |
223
+ "| \(.name) | \(.host):\(.port) \(.port_state) | " +
224
+ (if .health_path == null then "n/a" else ((.health_status // "000")|tostring) + "/" + ((.expected_status // 200)|tostring) end) +
225
+ " | " +
226
+ (if (.log.configured|not) then "not configured" elif (.log.exists|not) then "missing" else ((.log.recent_errors|tostring) + " recent errors") end) +
227
+ " | \(.status) |"' "$PROGRESS"
228
+ else
229
+ echo "No production services are configured for Service-Ops monitoring."
230
+ fi
231
+ echo ""
232
+ echo "## Discussion"
233
+ echo ""
234
+ case "$verdict" in
235
+ incident)
236
+ echo "- Service-Ops: down/degraded 서비스가 있으므로 feature work보다 장애 triage가 우선이다."
237
+ echo "- CTO: 복구 절차, 재시작, 설정 누락, 포트 충돌 중 원인을 좁히고 runtime-recovery work item을 받는다."
238
+ echo "- CQO: 복구 완료 주장은 health check와 재현 가능한 evidence 없이는 PASS로 보지 않는다."
239
+ echo "- COO: 장애 복구가 GOAL 진행을 막는 범위를 queue에 반영한다."
240
+ echo "- CEO: Owner에게 묻지 않고 내부 복구 루프를 진행한다. 단 GOAL 변경이 필요하면 별도 escalation한다."
241
+ ;;
242
+ owner_needed)
243
+ echo "- CEO: conductor escalation의 원인이 내부 해결 가능한지 우선 판정한다."
244
+ echo "- COO/CTO/CQO/Service-Ops: 각자 해결 가능한 내부 action을 제시하고, 불가능할 때만 Owner escalation을 유지한다."
245
+ ;;
246
+ autonomous_review)
247
+ echo "- CEO: Owner 입력은 interrupt이므로 대기하지 않는다."
248
+ echo "- COO: 다음 work package를 재정렬한다."
249
+ echo "- CTO/CQO/Service-Ops: 구현, 검증, 운영 관점에서 다음 handoff를 준비한다."
250
+ ;;
251
+ idle)
252
+ echo "- Meeting-Manager: idle은 Owner 대기가 아니라 operating cycle 재개 신호다."
253
+ echo "- COO: GOAL 대비 다음 후보 작업을 선정한다."
254
+ echo "- CTO/CQO/Service-Ops: 각자 실행/검증/운영 준비 상태를 점검한다."
255
+ ;;
256
+ *)
257
+ echo "- 회의 결론: 현재 GOAL을 유지하고 다음 autonomous tick에서 conductor가 다음 부서를 배정한다."
258
+ ;;
259
+ esac
260
+ echo ""
261
+ echo "## Decision JSON"
262
+ echo ""
263
+ echo '```json'
264
+ jq '.' <<<"$meeting_json"
265
+ echo '```'
266
+ echo ""
267
+ echo "## Action Items"
268
+ echo ""
269
+ case "$verdict" in
270
+ incident)
271
+ echo "- CTO: runtime recovery work item을 열고 down/degraded 서비스 복구 경로를 실행한다."
272
+ echo "- Service-Ops: 복구 후 monitor를 다시 돌려 last_check와 health evidence를 갱신한다."
273
+ echo "- CQO: 복구 evidence를 검증하고 regression risk를 기록한다."
274
+ ;;
275
+ idle|autonomous_review)
276
+ echo "- Planner/COO: Owner 대기 없이 active GOAL 기준 다음 work package를 정의한다."
277
+ echo "- Conductor: meeting decision을 읽고 다음 부서를 자동 배정한다."
278
+ ;;
279
+ *)
280
+ echo "- Conductor: 다음 autonomous tick에서 decision.owner/action_type 기준으로 handoff한다."
281
+ ;;
282
+ esac
283
+ } > "$meeting_path"
284
+
285
+ jq --arg ts "$ts" --arg id "$id" --arg rel "$meeting_rel" --arg verdict "$verdict" --arg type "$meeting_type" --argjson decision "$decision_json" '
286
+ .meetings.last_type = "hourly-review" |
287
+ .meetings.last_reason = $verdict |
288
+ .meetings.last_review_at = $ts |
289
+ .meetings.last_review_path = $rel |
290
+ .meetings.current_id = $id |
291
+ .meetings.current_record_path = $rel |
292
+ .meetings.last_attendees = ["dispatcher","planner","cto","cqo","service-ops","meeting-manager"] |
293
+ .meetings.decision = $decision |
294
+ if $verdict == "incident" or $verdict == "autonomous_review" or $verdict == "idle"
295
+ then .meetings.active = ((.meetings.active // []) + ["meeting-manager","dispatcher","planner","cto","cqo","service-ops"] | unique) |
296
+ .meetings.requested_type = (.meetings.requested_type // $type) |
297
+ .meetings.requested_reason = (.meetings.requested_reason // $verdict)
298
+ else . end |
299
+ .service_ops.monitor.last_hourly_review = $ts
300
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
301
+
302
+ echo "$ts | meeting-manager | hourly-review | $verdict | $id | $meeting_rel" >> "$log_path"
303
+ echo "$meeting_rel"