@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
|
@@ -36,7 +36,7 @@ default_decision_json() {
|
|
|
36
36
|
implementation_drift) owner="cto"; action_type="implement" ;;
|
|
37
37
|
planning_drift) owner="planner"; action_type="replan" ;;
|
|
38
38
|
ops_drift) owner="service-ops"; action_type="monitor" ;;
|
|
39
|
-
goal_drift) owner="
|
|
39
|
+
goal_drift) owner="planner"; action_type="goal-realignment" ;;
|
|
40
40
|
esac
|
|
41
41
|
if [ "$requested_reason" = "goal-intake" ]; then
|
|
42
42
|
owner="planner"
|
|
@@ -158,33 +158,43 @@ EOF
|
|
|
158
158
|
|
|
159
159
|
cat > "$meeting_dir/prep-dispatcher.md" <<'EOF'
|
|
160
160
|
# Prep — Dispatcher/CEO
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
161
|
+
- Role: 회사 내부 CEO. Owner와의 유일한 외부 창구이며, Owner 입력은 최초 GOAL 이후 interrupt/additional request로만 취급한다.
|
|
162
|
+
- Must answer: Goal 자체가 흔들렸는가, 아니면 내부 실행/품질/운영 문제인가?
|
|
163
|
+
- Must decide: Owner escalation 이 정말 필요한가, 아니면 내부 임원진이 처리할 수 있는가?
|
|
164
|
+
- Must not: Owner를 기다린다는 결론으로 회의를 끝내지 마라.
|
|
165
|
+
- Evidence to cite: active GOAL, KPI, previous decision, escalation trigger.
|
|
164
166
|
EOF
|
|
165
167
|
cat > "$meeting_dir/prep-planner.md" <<'EOF'
|
|
166
168
|
# Prep — Planner/COO
|
|
167
|
-
-
|
|
168
|
-
- planning_drift 또는 goal_drift
|
|
169
|
-
-
|
|
169
|
+
- Role: COO. GOAL을 실행 가능한 work package, 가설, queue로 바꾸고 조직이 옆길로 새지 않게 한다.
|
|
170
|
+
- Must answer: planning_drift 또는 goal_drift 인가, 아니면 구현/품질/운영 문제인가?
|
|
171
|
+
- Must decide: 다음 operating cycle에서 어떤 work package를 queue/backlog/track으로 만들 것인가?
|
|
172
|
+
- Must not: "다음 sprint에서", "Owner가 정하면" 같은 대기형 결론을 쓰지 마라.
|
|
173
|
+
- Evidence to cite: feature-list, sprint-contract, queue, hypothesis/report artifacts.
|
|
170
174
|
EOF
|
|
171
175
|
cat > "$meeting_dir/prep-cto.md" <<'EOF'
|
|
172
176
|
# Prep — CTO
|
|
173
|
-
-
|
|
174
|
-
- implementation_drift
|
|
175
|
-
- 어떤 generator/
|
|
177
|
+
- Role: CTO. 구현, 아키텍처, 기술선택, runtime recovery의 책임자다.
|
|
178
|
+
- Must answer: implementation_drift 인가, 런타임 장애인가, 설계 변경이 필요한가?
|
|
179
|
+
- Must decide: 어떤 generator/devops/hotfix worker를 태우고 어떤 deliverable을 요구할 것인가?
|
|
180
|
+
- Must not: 운영 장애를 단순 Service-Ops 알림으로만 남기지 마라. 복구 owner를 지정하라.
|
|
181
|
+
- Evidence to cite: code path, build/test output, runtime health, cto-review.
|
|
176
182
|
EOF
|
|
177
183
|
cat > "$meeting_dir/prep-cqo.md" <<'EOF'
|
|
178
184
|
# Prep — CQO
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
185
|
+
- Role: CQO. 품질, 회귀, 검증 기준의 최종 감시자다.
|
|
186
|
+
- Must answer: evidence가 충분한가, PASS/FAIL 판정이 방어 가능한가?
|
|
187
|
+
- Must decide: 어떤 evaluator/check가 필요하고, 어떤 주장은 검증 전까지 보류해야 하는가?
|
|
188
|
+
- Must not: evidence 없는 낙관론을 회의 결론으로 통과시키지 마라.
|
|
189
|
+
- Evidence to cite: evaluator reports, regression results, cqo-audit, reproducible checks.
|
|
182
190
|
EOF
|
|
183
191
|
cat > "$meeting_dir/prep-service-ops.md" <<'EOF'
|
|
184
192
|
# Prep — Service-Ops
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
-
|
|
193
|
+
- Role: Service-Ops. 운영 신호, KPI, incident, monitor cadence의 책임자다.
|
|
194
|
+
- Must answer: 어떤 KPI/로그/health/incident가 Goal에서 벗어났는가?
|
|
195
|
+
- Must decide: ops_drift 인가, incident-war-room 이 필요한가, monitor cadence를 바꿔야 하는가?
|
|
196
|
+
- Must not: 서버 down을 혼자 경고만 하고 끝내지 마라. 회의에 올려 CTO/CQO action으로 연결하라.
|
|
197
|
+
- Evidence to cite: ops-report path, health table, incident timeline, last_check.
|
|
188
198
|
EOF
|
|
189
199
|
|
|
190
200
|
decision_json="$(default_decision_json "$drift")"
|
|
@@ -217,6 +227,35 @@ $(jq '.prior_tracks' <<<"$resolved_fork_context")
|
|
|
217
227
|
- goal_adherence: $goal_adherence
|
|
218
228
|
$fork_section
|
|
219
229
|
|
|
230
|
+
## Required Role Positions
|
|
231
|
+
|
|
232
|
+
Meeting-Manager must fill this section before reporting the meeting as complete.
|
|
233
|
+
|
|
234
|
+
### Dispatcher/CEO
|
|
235
|
+
- Position:
|
|
236
|
+
- Evidence:
|
|
237
|
+
- Escalation needed: yes/no
|
|
238
|
+
|
|
239
|
+
### Planner/COO
|
|
240
|
+
- Position:
|
|
241
|
+
- Evidence:
|
|
242
|
+
- Proposed work package:
|
|
243
|
+
|
|
244
|
+
### CTO
|
|
245
|
+
- Position:
|
|
246
|
+
- Evidence:
|
|
247
|
+
- Engineering action:
|
|
248
|
+
|
|
249
|
+
### CQO
|
|
250
|
+
- Position:
|
|
251
|
+
- Evidence:
|
|
252
|
+
- Quality gate:
|
|
253
|
+
|
|
254
|
+
### Service-Ops
|
|
255
|
+
- Position:
|
|
256
|
+
- Evidence:
|
|
257
|
+
- Operational action:
|
|
258
|
+
|
|
220
259
|
## Decision JSON
|
|
221
260
|
\`\`\`json
|
|
222
261
|
$(jq -n \
|
package/scripts/harness-next.sh
CHANGED
|
@@ -196,7 +196,7 @@ run_pre_eval_gate() {
|
|
|
196
196
|
run_conductor_tick() {
|
|
197
197
|
local candidate="$1"
|
|
198
198
|
local owner
|
|
199
|
-
owner=$(jq -r '.
|
|
199
|
+
owner=$(jq -r '.company_mode.owner // "conductor"' "$CONFIG" 2>/dev/null || true)
|
|
200
200
|
if [ "$owner" != "conductor" ]; then return 0; fi
|
|
201
201
|
if [ ! -x "$SCRIPT_DIR/conductor-tick.sh" ]; then return 0; fi
|
|
202
202
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
# feature-queue.json을 생성/관리한다.
|
|
6
6
|
#
|
|
7
7
|
# Commands:
|
|
8
|
-
# init
|
|
8
|
+
# init [sprint|all] [project-root]
|
|
9
|
+
# feature-list.json → feature-queue.json 초기 생성
|
|
9
10
|
# dequeue <team> ready 큐에서 feature를 꺼내 team에 배정
|
|
10
11
|
# pass <fid> feature를 passed로 이동, blocked→ready 전이
|
|
11
12
|
# fail <fid> feature를 failed로 이동
|
|
@@ -71,8 +72,10 @@ fi
|
|
|
71
72
|
|
|
72
73
|
# ══════════════════════════════════════════
|
|
73
74
|
# init — Build queue from feature-list.json
|
|
74
|
-
# Usage: init [sprint_number]
|
|
75
|
-
# sprint_number: optional, filter features by sprint (default: all)
|
|
75
|
+
# Usage: init [sprint_number|all] [project-root]
|
|
76
|
+
# sprint_number: optional, filter features by sprint (default: all).
|
|
77
|
+
# If passing a project root, use `init all /path/to/project`; otherwise the
|
|
78
|
+
# first argument is interpreted as the sprint filter.
|
|
76
79
|
# ══════════════════════════════════════════
|
|
77
80
|
cmd_init() {
|
|
78
81
|
local sprint_filter="${1:-all}"
|
|
@@ -85,11 +88,15 @@ cmd_init() {
|
|
|
85
88
|
# Build dependency graph and topological sort
|
|
86
89
|
# Output: feature-queue.json with ready (no deps) and blocked (has deps)
|
|
87
90
|
jq --argjson concurrency "$CONCURRENCY" --arg sprint "$sprint_filter" '
|
|
88
|
-
# Build passed set (features already passed by evaluator)
|
|
91
|
+
# Build passed set (features already passed by evaluator or imported audit).
|
|
92
|
+
# Legacy projects use string passes; newer NEXUS artifacts may use objects.
|
|
89
93
|
def passed_set:
|
|
90
94
|
[.features[] | select(
|
|
91
95
|
((.passes // []) | length > 0) and
|
|
92
|
-
((.passes // []) | any(
|
|
96
|
+
((.passes // []) | any(
|
|
97
|
+
(. == "evaluator-functional") or
|
|
98
|
+
((type == "object") and ((.status // "") == "PASS"))
|
|
99
|
+
))
|
|
93
100
|
) | .id] ;
|
|
94
101
|
|
|
95
102
|
# Filter features by sprint if specified
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-service-ops-monitor.sh — deterministic Service-Ops production check
|
|
3
|
+
#
|
|
4
|
+
# Reads .harness/config.json runtime.production.services[] and writes:
|
|
5
|
+
# - progress.json.service_ops.health[]
|
|
6
|
+
# - progress.json.service_ops.monitor.last_check
|
|
7
|
+
# - .harness/actions/ops-report-hourly-<timestamp>.md
|
|
8
|
+
#
|
|
9
|
+
# This script is intentionally deterministic and does not require an LLM. Claude
|
|
10
|
+
# can add analysis later, but the Owner must always have a disk-backed record.
|
|
11
|
+
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
source "$SCRIPT_DIR/lib/harness-render-progress.sh"
|
|
16
|
+
|
|
17
|
+
PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || exit 0
|
|
18
|
+
PROGRESS="$PROJECT_ROOT/.harness/progress.json"
|
|
19
|
+
CONFIG="$PROJECT_ROOT/.harness/config.json"
|
|
20
|
+
ACTIONS_DIR="$PROJECT_ROOT/.harness/actions"
|
|
21
|
+
OPS_DIR="$PROJECT_ROOT/.harness/ops"
|
|
22
|
+
|
|
23
|
+
[ -f "$PROGRESS" ] || exit 0
|
|
24
|
+
[ -f "$CONFIG" ] || exit 0
|
|
25
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
26
|
+
|
|
27
|
+
mkdir -p "$ACTIONS_DIR" "$OPS_DIR"
|
|
28
|
+
|
|
29
|
+
live=$(jq -r '.runtime.production.live // false' "$CONFIG" 2>/dev/null || echo false)
|
|
30
|
+
service_count=$(jq -r '(.runtime.production.services // []) | length' "$CONFIG" 2>/dev/null || echo 0)
|
|
31
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
32
|
+
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
33
|
+
report_rel=".harness/actions/ops-report-hourly-${stamp}.md"
|
|
34
|
+
report_path="$PROJECT_ROOT/$report_rel"
|
|
35
|
+
|
|
36
|
+
json_escape() {
|
|
37
|
+
jq -Rn --arg v "$1" '$v'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
tcp_check() {
|
|
41
|
+
local host="$1"
|
|
42
|
+
local port="$2"
|
|
43
|
+
if command -v nc >/dev/null 2>&1; then
|
|
44
|
+
nc -z -w 2 "$host" "$port" >/dev/null 2>&1
|
|
45
|
+
return $?
|
|
46
|
+
fi
|
|
47
|
+
bash -c ":</dev/tcp/$host/$port" >/dev/null 2>&1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
http_status() {
|
|
51
|
+
local url="$1"
|
|
52
|
+
if command -v curl >/dev/null 2>&1; then
|
|
53
|
+
curl -s -o /dev/null -m 3 -w "%{http_code}" "$url" 2>/dev/null || echo "000"
|
|
54
|
+
else
|
|
55
|
+
echo "000"
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
log_summary_json() {
|
|
60
|
+
local log_path="$1"
|
|
61
|
+
if [ -z "$log_path" ] || [ "$log_path" = "null" ]; then
|
|
62
|
+
jq -n '{configured:false, exists:false, recent_errors:0, last_line:null}'
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
if [ ! -f "$log_path" ]; then
|
|
66
|
+
jq -n --arg path "$log_path" '{configured:true, exists:false, path:$path, recent_errors:0, last_line:null}'
|
|
67
|
+
return
|
|
68
|
+
fi
|
|
69
|
+
local recent errors last_line
|
|
70
|
+
recent="$(tail -200 "$log_path" 2>/dev/null || true)"
|
|
71
|
+
errors="$(printf '%s\n' "$recent" | grep -Eic 'error|exception|fatal|panic|traceback|fail' || true)"
|
|
72
|
+
last_line="$(tail -1 "$log_path" 2>/dev/null || true)"
|
|
73
|
+
jq -n --arg path "$log_path" --arg last "$last_line" --argjson errors "${errors:-0}" \
|
|
74
|
+
'{configured:true, exists:true, path:$path, recent_errors:$errors, last_line:$last}'
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if [ "$live" != "true" ] || [ "${service_count:-0}" -eq 0 ]; then
|
|
78
|
+
jq --arg ts "$ts" --arg report "$report_rel" '
|
|
79
|
+
.service_ops.monitor.last_check = $ts |
|
|
80
|
+
.service_ops.monitor.stream_active = false |
|
|
81
|
+
.service_ops.monitor.last_report = $report |
|
|
82
|
+
.service_ops.health = []
|
|
83
|
+
' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
echo "# Service-Ops Hourly Report"
|
|
87
|
+
echo ""
|
|
88
|
+
echo "- ts: $ts"
|
|
89
|
+
echo "- production.live: $live"
|
|
90
|
+
echo "- services: $service_count"
|
|
91
|
+
echo "- verdict: NO_PRODUCTION_SERVICES"
|
|
92
|
+
echo ""
|
|
93
|
+
echo "No production service endpoints are configured. Owner+CEO+CTO must define runtime.production.services[] before Service-Ops can monitor live servers."
|
|
94
|
+
} > "$report_path"
|
|
95
|
+
echo "$report_rel"
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
results_file="$(mktemp)"
|
|
100
|
+
: > "$results_file"
|
|
101
|
+
|
|
102
|
+
i=0
|
|
103
|
+
while [ "$i" -lt "$service_count" ]; do
|
|
104
|
+
svc="$(jq -c ".runtime.production.services[$i]" "$CONFIG")"
|
|
105
|
+
name="$(jq -r '.name // ("service-" + (input_line_number|tostring))' <<<"$svc")"
|
|
106
|
+
host="$(jq -r '.host // "127.0.0.1"' <<<"$svc")"
|
|
107
|
+
port="$(jq -r '.port // empty' <<<"$svc")"
|
|
108
|
+
health_path="$(jq -r '.health_path // empty' <<<"$svc")"
|
|
109
|
+
expected_status="$(jq -r '.expected_status // 200' <<<"$svc")"
|
|
110
|
+
log_path="$(jq -r '.log_path // empty' <<<"$svc")"
|
|
111
|
+
|
|
112
|
+
port_state="unknown"
|
|
113
|
+
if [ -n "$port" ] && tcp_check "$host" "$port"; then
|
|
114
|
+
port_state="listening"
|
|
115
|
+
else
|
|
116
|
+
port_state="down"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
health_status="null"
|
|
120
|
+
health_ok="null"
|
|
121
|
+
health_url=""
|
|
122
|
+
if [ -n "$health_path" ] && [ "$health_path" != "null" ] && [ -n "$port" ]; then
|
|
123
|
+
health_url="http://${host}:${port}${health_path}"
|
|
124
|
+
health_status="$(http_status "$health_url")"
|
|
125
|
+
if [ "$health_status" = "$expected_status" ]; then health_ok="true"; else health_ok="false"; fi
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
log_json="$(log_summary_json "$log_path")"
|
|
129
|
+
|
|
130
|
+
status="ok"
|
|
131
|
+
if [ "$port_state" != "listening" ]; then
|
|
132
|
+
status="down"
|
|
133
|
+
elif [ "$health_ok" = "false" ]; then
|
|
134
|
+
status="degraded"
|
|
135
|
+
elif [ "$(jq -r '.configured and (.exists|not)' <<<"$log_json")" = "true" ]; then
|
|
136
|
+
status="log_missing"
|
|
137
|
+
elif [ "$(jq -r '.recent_errors // 0' <<<"$log_json")" -gt 0 ]; then
|
|
138
|
+
status="warn"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
jq -n \
|
|
142
|
+
--arg ts "$ts" \
|
|
143
|
+
--arg name "$name" \
|
|
144
|
+
--arg host "$host" \
|
|
145
|
+
--arg port "$port" \
|
|
146
|
+
--arg port_state "$port_state" \
|
|
147
|
+
--arg health_path "$health_path" \
|
|
148
|
+
--arg health_url "$health_url" \
|
|
149
|
+
--arg expected_status "$expected_status" \
|
|
150
|
+
--arg health_status "$health_status" \
|
|
151
|
+
--arg health_ok "$health_ok" \
|
|
152
|
+
--arg status "$status" \
|
|
153
|
+
--argjson log "$log_json" \
|
|
154
|
+
'{
|
|
155
|
+
ts:$ts,
|
|
156
|
+
name:$name,
|
|
157
|
+
host:$host,
|
|
158
|
+
port:($port|tonumber?),
|
|
159
|
+
port_state:$port_state,
|
|
160
|
+
health_path:(if $health_path == "" or $health_path == "null" then null else $health_path end),
|
|
161
|
+
health_url:(if $health_url == "" then null else $health_url end),
|
|
162
|
+
expected_status:($expected_status|tonumber?),
|
|
163
|
+
health_status:(if $health_status == "null" then null else ($health_status|tonumber?) end),
|
|
164
|
+
health_ok:(if $health_ok == "true" then true elif $health_ok == "false" then false else null end),
|
|
165
|
+
log:$log,
|
|
166
|
+
status:$status
|
|
167
|
+
}' >> "$results_file"
|
|
168
|
+
i=$((i + 1))
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
results_json="$(jq -s '.' "$results_file")"
|
|
172
|
+
rm -f "$results_file"
|
|
173
|
+
|
|
174
|
+
down_count="$(jq '[.[] | select(.status == "down" or .status == "degraded")] | length' <<<"$results_json")"
|
|
175
|
+
warn_count="$(jq '[.[] | select(.status == "warn" or .status == "log_missing")] | length' <<<"$results_json")"
|
|
176
|
+
ok_count="$(jq '[.[] | select(.status == "ok")] | length' <<<"$results_json")"
|
|
177
|
+
verdict="OK"
|
|
178
|
+
if [ "$down_count" -gt 0 ]; then verdict="INCIDENT"; elif [ "$warn_count" -gt 0 ]; then verdict="WARN"; fi
|
|
179
|
+
|
|
180
|
+
incidents_json="$(jq '
|
|
181
|
+
[ .[] | select(.status == "down" or .status == "degraded") |
|
|
182
|
+
{
|
|
183
|
+
id: ("OPS-" + (.name | ascii_upcase | gsub("[^A-Z0-9]+";"-"))),
|
|
184
|
+
dept: "Operations",
|
|
185
|
+
severity: (if .status == "down" then "critical" else "high" end),
|
|
186
|
+
message: (.name + " " + .status + " (" + .host + ":" + (.port|tostring) + ")"),
|
|
187
|
+
ts: .ts
|
|
188
|
+
}
|
|
189
|
+
]' <<<"$results_json")"
|
|
190
|
+
|
|
191
|
+
jq --arg ts "$ts" --arg report "$report_rel" --argjson health "$results_json" --argjson incidents "$incidents_json" --argjson warn "$warn_count" --argjson alert "$down_count" '
|
|
192
|
+
.service_ops.monitor.stream_active = false |
|
|
193
|
+
.service_ops.monitor.last_check = $ts |
|
|
194
|
+
.service_ops.monitor.last_report = $report |
|
|
195
|
+
.service_ops.monitor.warns_this_sprint = ((.service_ops.monitor.warns_this_sprint // 0) + $warn) |
|
|
196
|
+
.service_ops.monitor.alerts_this_sprint = ((.service_ops.monitor.alerts_this_sprint // 0) + $alert) |
|
|
197
|
+
.service_ops.health = $health |
|
|
198
|
+
.service_ops.incident.open = $incidents
|
|
199
|
+
' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
echo "# Service-Ops Hourly Report"
|
|
203
|
+
echo ""
|
|
204
|
+
echo "- ts: $ts"
|
|
205
|
+
echo "- production.live: true"
|
|
206
|
+
echo "- services: $service_count"
|
|
207
|
+
echo "- verdict: $verdict"
|
|
208
|
+
echo "- ok: $ok_count"
|
|
209
|
+
echo "- warnings: $warn_count"
|
|
210
|
+
echo "- alerts: $down_count"
|
|
211
|
+
echo ""
|
|
212
|
+
echo "| Service | Port | Health | Logs | Status |"
|
|
213
|
+
echo "|---|---:|---|---|---|"
|
|
214
|
+
jq -r '.[] |
|
|
215
|
+
"| \(.name) | \(.host):\(.port) \(.port_state) | " +
|
|
216
|
+
(if .health_path == null then "n/a" else ((.health_status // "000")|tostring) + " expected " + ((.expected_status // 200)|tostring) end) +
|
|
217
|
+
" | " +
|
|
218
|
+
(if (.log.configured|not) then "not configured" elif (.log.exists|not) then "missing" else ((.log.recent_errors|tostring) + " recent errors") end) +
|
|
219
|
+
" | \(.status) |"' <<<"$results_json"
|
|
220
|
+
echo ""
|
|
221
|
+
echo "## Raw"
|
|
222
|
+
echo ""
|
|
223
|
+
echo '```json'
|
|
224
|
+
jq '.' <<<"$results_json"
|
|
225
|
+
echo '```'
|
|
226
|
+
} > "$report_path"
|
|
227
|
+
|
|
228
|
+
echo "$report_rel"
|
|
@@ -32,7 +32,7 @@ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
|
|
|
32
32
|
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
33
33
|
next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
34
34
|
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
35
|
-
mode=$(jq -r '.mode // "
|
|
35
|
+
mode=$(jq -r '.mode // "company"' "$PROGRESS" 2>/dev/null)
|
|
36
36
|
conductor_state=$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null)
|
|
37
37
|
task_stop_active=$(jq -r '.task_stop.active // false' "$PROGRESS" 2>/dev/null)
|
|
38
38
|
task_stop_reason=$(jq -r '.task_stop.reason // "null"' "$PROGRESS" 2>/dev/null)
|
|
@@ -53,32 +53,65 @@ now_epoch() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
# ─────────────────────────────────────────
|
|
56
|
-
#
|
|
57
|
-
# feature-queue.json 에 활성 작업(in_progress)이 있는데 mode 가 solo/paused 면
|
|
58
|
-
# 누군가 progress.json 을 통째로 덮어써서 team_state 를 날린 것. mode=team 으로
|
|
59
|
-
# 자동 복원하고 경고 로그 남김.
|
|
56
|
+
# Normalize legacy mode drift — v6.3+ always runs company mode.
|
|
60
57
|
# ─────────────────────────────────────────
|
|
61
58
|
FEATURE_QUEUE_HEAL="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
62
|
-
if [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
if [ "$mode" != "company" ]; then
|
|
60
|
+
bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
|
|
61
|
+
'.mode = "company" |
|
|
62
|
+
.mode_decision.owner = "conductor" |
|
|
63
|
+
.mode_decision.policy = "always_company_parallel" |
|
|
64
|
+
.mode_decision.user_override = null |
|
|
65
|
+
.mode_decision.decided_at = (now | todate) |
|
|
66
|
+
.mode_decision.rationale = "legacy mode normalized to always-on company mode"' \
|
|
67
|
+
2>/dev/null || true
|
|
68
|
+
mode="company"
|
|
69
|
+
[ -f "$PROJECT_ROOT/.harness/progress.log" ] && echo "$(date +'%Y-%m-%d %H:%M') | system | heal | mode | normalized legacy mode to company" >> "$PROJECT_ROOT/.harness/progress.log"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ─────────────────────────────────────────
|
|
73
|
+
# Truthful Logging Heal — strip future-dated progress.log entries
|
|
74
|
+
# ─────────────────────────────────────────
|
|
75
|
+
PROGRESS_LOG="$PROJECT_ROOT/.harness/progress.log"
|
|
76
|
+
if [ -f "$PROGRESS_LOG" ]; then
|
|
77
|
+
NOW_KST=$(date "+%Y-%m-%d %H:%M")
|
|
78
|
+
NOW_UTC_MIN=$(date -u "+%Y-%m-%d %H:%M")
|
|
79
|
+
QUARANTINE_FILE="$PROGRESS_LOG.future-quarantine.$(date +%s)"
|
|
80
|
+
KEEP_FILE="$PROGRESS_LOG.tmp.$$"
|
|
81
|
+
|
|
82
|
+
# 미래 시각으로 시작하는 라인 격리. KST(YYYY-MM-DD HH:MM) 와 ISO(YYYY-MM-DDTHH:MM:SSZ) 둘 다 처리.
|
|
83
|
+
awk -v now_kst="$NOW_KST" -v now_utc="$NOW_UTC_MIN" -v qfile="$QUARANTINE_FILE" '
|
|
84
|
+
function iso_to_minute(s) {
|
|
85
|
+
gsub("\\[", "", s); gsub("T", " ", s); gsub("Z", "", s)
|
|
86
|
+
return substr(s, 1, 16)
|
|
87
|
+
}
|
|
88
|
+
{
|
|
89
|
+
future=0
|
|
90
|
+
if (match($0, /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}/)) {
|
|
91
|
+
ts = substr($0, RSTART, RLENGTH)
|
|
92
|
+
if (ts > now_kst) future=1
|
|
93
|
+
} else if (match($0, /^\[?[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}/)) {
|
|
94
|
+
ts_min = iso_to_minute(substr($0, RSTART, RLENGTH+5))
|
|
95
|
+
if (ts_min > now_utc) future=1
|
|
96
|
+
}
|
|
97
|
+
if (future) print >> qfile
|
|
98
|
+
else print
|
|
99
|
+
}
|
|
100
|
+
' "$PROGRESS_LOG" > "$KEEP_FILE"
|
|
101
|
+
|
|
102
|
+
if [ -f "$QUARANTINE_FILE" ] && [ -s "$QUARANTINE_FILE" ]; then
|
|
103
|
+
QCOUNT=$(wc -l < "$QUARANTINE_FILE" | tr -d ' ')
|
|
104
|
+
mv "$KEEP_FILE" "$PROGRESS_LOG"
|
|
105
|
+
echo "$(date +'%Y-%m-%d %H:%M') | system | heal | log | quarantined ${QCOUNT} future-dated lines → $(basename "$QUARANTINE_FILE")" >> "$PROGRESS_LOG"
|
|
106
|
+
else
|
|
107
|
+
rm -f "$KEEP_FILE" "$QUARANTINE_FILE" 2>/dev/null
|
|
75
108
|
fi
|
|
76
109
|
fi
|
|
77
110
|
|
|
78
111
|
# ─────────────────────────────────────────
|
|
79
|
-
#
|
|
112
|
+
# Company Mode — autonomous parallel company loop
|
|
80
113
|
# ─────────────────────────────────────────
|
|
81
|
-
if [ "$mode" = "
|
|
114
|
+
if [ "$mode" = "company" ]; then
|
|
82
115
|
FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
83
116
|
passed=0; total=0; in_prog=0; failed=0
|
|
84
117
|
if [ -f "$FEATURE_QUEUE" ]; then
|
|
@@ -88,18 +121,9 @@ if [ "$mode" = "team" ]; then
|
|
|
88
121
|
failed=$(jq '.queue.failed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
89
122
|
fi
|
|
90
123
|
|
|
91
|
-
echo "# Harness
|
|
124
|
+
echo "# Harness Company Mode active"
|
|
92
125
|
echo "# Queue: ${passed}/${total} passed, ${in_prog} in progress, ${failed} failed"
|
|
93
|
-
echo "#
|
|
94
|
-
echo "# Stop: /harness-stop | Emergency fallback: /harness-solo"
|
|
95
|
-
exit 0
|
|
96
|
-
fi
|
|
97
|
-
|
|
98
|
-
# ─────────────────────────────────────────
|
|
99
|
-
# Paused Mode — 중단 상태 안내
|
|
100
|
-
# ─────────────────────────────────────────
|
|
101
|
-
if [ "$mode" = "paused" ]; then
|
|
102
|
-
echo "# Harness paused — resume with /harness-team"
|
|
126
|
+
echo "# Company loop runs autonomously with parallel workers. Owner input is an interrupt only; do not wait for Owner after the initial GOAL."
|
|
103
127
|
exit 0
|
|
104
128
|
fi
|
|
105
129
|
|
|
@@ -144,9 +168,14 @@ fi
|
|
|
144
168
|
# init 상태: 첫 안내
|
|
145
169
|
# ─────────────────────────────────────────
|
|
146
170
|
if [ "$sprint_status" = "init" ]; then
|
|
171
|
+
if [ "$next_agent" != "null" ] && [ "$next_agent" != "none" ] && [ "$next_agent" != "dispatcher" ]; then
|
|
172
|
+
echo "# Harness Company Mode active"
|
|
173
|
+
echo "# Sprint is init, but next_agent=${next_agent}; continue the autonomous loop instead of waiting for Owner."
|
|
174
|
+
else
|
|
147
175
|
echo "# Harness ready — say \"하네스 엔지니어링 시작\" or /harness-dispatcher"
|
|
148
176
|
echo "# 기본 경로는 회사 루프입니다: dispatch -> CEO meeting -> COO/CTO 분배 -> gen/eval -> CQO -> service-ops -> batch meeting."
|
|
149
177
|
exit 0
|
|
178
|
+
fi
|
|
150
179
|
fi
|
|
151
180
|
|
|
152
181
|
# ─────────────────────────────────────────
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# harness-statusline.sh — Claude Code statusline hook (v5 unified)
|
|
3
3
|
# 터미널 하단에 항상 고정되는 1줄 compact 상태 표시.
|
|
4
|
-
#
|
|
5
|
-
# Team: [S1] FULL | team | T1:gen T2:eval T3:idle | 2/5 feat | ctx 45% | $1.23
|
|
4
|
+
# Company: [C1] FULL | company | T1:gen T2:eval T3:idle | 2/5 work | ctx 45% | $1.23
|
|
6
5
|
|
|
7
6
|
# Read Claude Code session data from stdin
|
|
8
7
|
input=$(cat)
|
|
@@ -32,14 +31,13 @@ if [ ! -f "$PROGRESS" ]; then
|
|
|
32
31
|
fi
|
|
33
32
|
|
|
34
33
|
# Read harness state
|
|
35
|
-
|
|
36
|
-
sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
|
|
34
|
+
cycle_num=$(jq -r '.sprint.number // .cycle.number // 0' "$PROGRESS" 2>/dev/null)
|
|
37
35
|
pipeline=$(jq -r '.pipeline // "?"' "$PROGRESS" 2>/dev/null)
|
|
38
36
|
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
39
37
|
agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null)
|
|
40
38
|
next_agent=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null)
|
|
41
39
|
retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS" 2>/dev/null)
|
|
42
|
-
harness_mode
|
|
40
|
+
harness_mode="company"
|
|
43
41
|
|
|
44
42
|
# Pipeline short name
|
|
45
43
|
case "$pipeline" in
|
|
@@ -111,8 +109,8 @@ fi
|
|
|
111
109
|
# Build compact status line based on mode
|
|
112
110
|
FEATURE_QUEUE="$PROJECT_ROOT/.harness/actions/feature-queue.json"
|
|
113
111
|
|
|
114
|
-
if [
|
|
115
|
-
#
|
|
112
|
+
if [ -f "$FEATURE_QUEUE" ]; then
|
|
113
|
+
# Company mode: show worker status instead of single agent
|
|
116
114
|
team_info=""
|
|
117
115
|
for i in 1 2 3; do
|
|
118
116
|
t_status=$(jq -r ".teams[\"$i\"].status // \"idle\"" "$FEATURE_QUEUE" 2>/dev/null)
|
|
@@ -127,13 +125,11 @@ if [ "$harness_mode" = "team" ] && [ -f "$FEATURE_QUEUE" ]; then
|
|
|
127
125
|
t_passed=$(jq '.queue.passed | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
128
126
|
t_total=$(jq '[.queue.ready, (.queue.blocked | keys), (.queue.in_progress | keys), .queue.passed, .queue.failed] | flatten | length' "$FEATURE_QUEUE" 2>/dev/null || echo 0)
|
|
129
127
|
|
|
130
|
-
echo "[
|
|
128
|
+
echo "[C${cycle_num}] ${pl} | company | ${team_info}| ${t_passed}/${t_total} work | ctx ${context_pct}% | \$${cost}"
|
|
131
129
|
else
|
|
132
|
-
# Solo/paused mode: show single agent status
|
|
133
|
-
mode_tag="${harness_mode}"
|
|
134
130
|
if [ -n "$model_short" ]; then
|
|
135
|
-
echo "[
|
|
131
|
+
echo "[C${cycle_num}] ${pl} | company | ${status_icon}${agent_short}${retry_str} | ${model_short} | ${completed_features}/${total_features} work | ctx ${context_pct}% | \$${cost}"
|
|
136
132
|
else
|
|
137
|
-
echo "[
|
|
133
|
+
echo "[C${cycle_num}] ${pl} | company | ${status_icon}${agent_short}${retry_str} | ${completed_features}/${total_features} work | ctx ${context_pct}% | \$${cost}"
|
|
138
134
|
fi
|
|
139
135
|
fi
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# harness-stop.sh — Stop hook (v6.2)
|
|
3
|
+
#
|
|
4
|
+
# Claude 가 한 turn 을 끝내려 할 때 발화.
|
|
5
|
+
# conductor.state == "running" 이고 다음 작업이 남아 있으면
|
|
6
|
+
# stdout 으로 {"decision":"block", "reason":"..."} JSON 을 출력해
|
|
7
|
+
# Claude 가 멈추지 않고 다음 tick 으로 자동 연쇄하게 한다.
|
|
8
|
+
#
|
|
9
|
+
# 멈춰야 할 경우 (회사 루프 정지·task_stop 등) 는 그냥 exit 0
|
|
10
|
+
# → Claude 가 정상적으로 turn 을 종료.
|
|
11
|
+
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat 2>/dev/null || echo "{}")
|
|
15
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
16
|
+
[ -z "$CWD" ] && CWD="$PWD"
|
|
17
|
+
|
|
18
|
+
# 하네스 초기화 안 된 곳: no-op
|
|
19
|
+
[ -f "$CWD/.harness/progress.json" ] || exit 0
|
|
20
|
+
[ -f "$CWD/.harness/config.json" ] || exit 0
|
|
21
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
22
|
+
|
|
23
|
+
PROGRESS="$CWD/.harness/progress.json"
|
|
24
|
+
CONFIG="$CWD/.harness/config.json"
|
|
25
|
+
|
|
26
|
+
# Opt-out
|
|
27
|
+
AUTO_CHAIN=$(jq -r '.behavior.auto_chain_on_stop // true' "$CONFIG" 2>/dev/null || echo "true")
|
|
28
|
+
if [ "$AUTO_CHAIN" != "true" ]; then exit 0; fi
|
|
29
|
+
|
|
30
|
+
# 무한루프 방지: 한 sprint 안에서 stop_chain_count 가 상한을 넘으면 중단
|
|
31
|
+
STOP_CHAIN_MAX=$(jq -r '.behavior.auto_chain_max_per_sprint // 200' "$CONFIG" 2>/dev/null || echo 200)
|
|
32
|
+
STOP_CHAIN_COUNT=$(jq -r '.conductor.stop_chain_count // 0' "$PROGRESS" 2>/dev/null || echo 0)
|
|
33
|
+
if [ "${STOP_CHAIN_COUNT:-0}" -ge "${STOP_CHAIN_MAX:-200}" ]; then exit 0; fi
|
|
34
|
+
|
|
35
|
+
# 회사 루프 상태 확인
|
|
36
|
+
CONDUCTOR_STATE=$(jq -r '.conductor.state // "idle"' "$PROGRESS" 2>/dev/null || echo "idle")
|
|
37
|
+
NEXT_AGENT=$(jq -r '.next_agent // "none"' "$PROGRESS" 2>/dev/null || echo "none")
|
|
38
|
+
AGENT_STATUS=$(jq -r '.agent_status // "pending"' "$PROGRESS" 2>/dev/null || echo "pending")
|
|
39
|
+
TASK_STOP_ACTIVE=$(jq -r '.task_stop.active // false' "$PROGRESS" 2>/dev/null || echo "false")
|
|
40
|
+
ESCALATION=$(jq -r '.conductor.escalation // "null"' "$PROGRESS" 2>/dev/null || echo "null")
|
|
41
|
+
SPRINT_STATUS=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null || echo "init")
|
|
42
|
+
|
|
43
|
+
# stop hook 이 자기 자신에 의해 재진입되지 않도록 hook event 필터링
|
|
44
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null || true)
|
|
45
|
+
if [ "$HOOK_EVENT" = "Stop" ]; then
|
|
46
|
+
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
|
47
|
+
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then exit 0; fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# 자동 연쇄 조건:
|
|
51
|
+
# conductor 가 running
|
|
52
|
+
# AND task_stop 비활성
|
|
53
|
+
# AND task_stop 비활성
|
|
54
|
+
# AND (next_agent 가 명시되어 있거나, agent_status=completed 라서 다음 tick 결정 필요)
|
|
55
|
+
should_chain="false"
|
|
56
|
+
if [ "$CONDUCTOR_STATE" = "running" ] \
|
|
57
|
+
&& [ "$TASK_STOP_ACTIVE" != "true" ]; then
|
|
58
|
+
if [ "$NEXT_AGENT" != "none" ] && [ "$NEXT_AGENT" != "null" ]; then
|
|
59
|
+
should_chain="true"
|
|
60
|
+
elif [ "$AGENT_STATUS" = "completed" ]; then
|
|
61
|
+
should_chain="true"
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ "$should_chain" != "true" ]; then exit 0; fi
|
|
66
|
+
|
|
67
|
+
# stop_chain_count 증가 (느슨한 카운터 — race 허용)
|
|
68
|
+
NEW_COUNT=$((STOP_CHAIN_COUNT + 1))
|
|
69
|
+
TMP=$(mktemp)
|
|
70
|
+
jq --argjson n "$NEW_COUNT" '.conductor.stop_chain_count = $n |
|
|
71
|
+
.conductor.last_stop_chain_at = (now | todate)' "$PROGRESS" > "$TMP" 2>/dev/null \
|
|
72
|
+
&& mv "$TMP" "$PROGRESS" || rm -f "$TMP"
|
|
73
|
+
|
|
74
|
+
# Claude 에게 다음 행동 지시
|
|
75
|
+
if [ "$ESCALATION" != "null" ]; then
|
|
76
|
+
REASON="자율 회사 루프 진행 중. escalation=${ESCALATION} 는 정지 사유가 아니라 meeting-manager/service-ops 공유 대상이다. next_agent=${NEXT_AGENT}, agent_status=${AGENT_STATUS}, conductor=${CONDUCTOR_STATE}. SKILL 에 따라 conductor-tick 을 굴려 meeting-manager 회의로 라우팅하거나 다음 부서를 spawn 하라. progress.log 에는 절대 미래 시각을 쓰지 말 것. 진짜로 끝났을 때만 turn 을 종료하라."
|
|
77
|
+
else
|
|
78
|
+
REASON="자율 회사 루프 진행 중. next_agent=${NEXT_AGENT}, agent_status=${AGENT_STATUS}, conductor=${CONDUCTOR_STATE}. SKILL 에 따라 다음 부서를 spawn 하거나 conductor-tick 을 한 번 더 굴려라. progress.log 에는 절대 미래 시각을 쓰지 말 것. 진짜로 끝났을 때만 turn 을 종료하라."
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
jq -nc \
|
|
82
|
+
--arg reason "$REASON" \
|
|
83
|
+
'{decision: "block", reason: $reason}'
|
|
84
|
+
|
|
85
|
+
exit 0
|