@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.
- package/CHANGELOG.md +28 -0
- package/README.md +43 -74
- package/assets/launchd/com.walwal.harness-wake.plist.template +32 -0
- package/assets/templates/CONVENTIONS.md +12 -0
- package/assets/templates/HARNESS.md +8 -8
- package/assets/templates/config.json +25 -27
- package/assets/templates/memory.md +16 -2
- package/assets/templates/progress.json.template +6 -5
- package/bin/init.js +179 -90
- package/conventions/shared.md +16 -0
- package/gotchas/conductor.md +16 -0
- package/gotchas/dispatcher.md +16 -0
- package/gotchas/meeting-manager.md +8 -0
- package/gotchas/service-ops.md +8 -0
- package/package.json +4 -4
- package/scripts/conductor-tick.sh +61 -53
- package/scripts/harness-archive.sh +7 -5
- package/scripts/harness-dashboard-up.sh +11 -3
- package/scripts/harness-hourly-review.sh +303 -0
- package/scripts/harness-meeting-doc.sh +55 -16
- package/scripts/harness-next.sh +1 -1
- package/scripts/harness-queue-manager.sh +12 -5
- package/scripts/harness-service-ops-monitor.sh +228 -0
- package/scripts/harness-session-start.sh +60 -31
- package/scripts/harness-statusline.sh +8 -12
- package/scripts/harness-stop.sh +85 -0
- package/scripts/harness-user-prompt-submit.sh +11 -29
- package/scripts/harness-wake-install.sh +179 -0
- package/scripts/harness-wake.sh +258 -0
- package/scripts/harness-worker-dispatch.sh +157 -0
- package/scripts/lib/harness-progress-migrate.sh +6 -1
- package/scripts/lib/harness-render-progress.sh +3 -3
- package/skills/brainstorming/SKILL.md +2 -2
- package/skills/conductor/SKILL.md +67 -55
- package/skills/cqo/SKILL.md +1 -0
- package/skills/cto/SKILL.md +1 -0
- package/skills/dispatcher/SKILL.md +33 -14
- package/skills/evaluator-code-quality/SKILL.md +4 -4
- package/skills/evaluator-functional/SKILL.md +6 -6
- package/skills/evaluator-visual/SKILL.md +4 -4
- package/skills/generator-backend/SKILL.md +4 -4
- package/skills/generator-frontend/SKILL.md +5 -5
- package/skills/meeting-manager/SKILL.md +82 -12
- package/skills/planner/SKILL.md +3 -3
- package/skills/service-ops/SKILL.md +25 -0
- package/commands/harness-solo.md +0 -103
- package/commands/harness-stop.md +0 -53
- package/commands/harness-team.md +0 -530
- package/scripts/harness-dashboard.sh +0 -509
- package/scripts/harness-goal-init.sh +0 -72
- package/scripts/harness-goal-show.sh +0 -37
- package/scripts/harness-gotcha-memory.sh +0 -348
- package/scripts/harness-monitor.sh +0 -398
- package/scripts/harness-prompt-history.sh +0 -164
- 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 // "
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.mode_decision.
|
|
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 =
|
|
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
|
-
|
|
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="
|
|
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
|
|
322
|
-
# the first
|
|
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
|
-
|
|
328
|
-
|
|
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:
|
|
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:
|
|
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 = \"
|
|
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 = "
|
|
92
|
-
| .
|
|
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)
|
|
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"
|