@walwal-harness/cli 6.0.5 → 6.1.0

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 (43) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/commands/harness-solo.md +20 -20
  3. package/commands/harness-team.md +1 -1
  4. package/package.json +1 -1
  5. package/scripts/conductor-tick.sh +382 -0
  6. package/scripts/harness-archive.sh +123 -0
  7. package/scripts/harness-dashboard-up.sh +72 -0
  8. package/scripts/harness-dashboard.sh +509 -0
  9. package/scripts/harness-goal-init.sh +72 -0
  10. package/scripts/harness-goal-show.sh +37 -0
  11. package/scripts/harness-gotcha-memory.sh +348 -0
  12. package/scripts/harness-gotcha-register.sh +393 -0
  13. package/scripts/harness-meeting-doc.sh +153 -0
  14. package/scripts/harness-monitor.sh +398 -0
  15. package/scripts/harness-next.sh +577 -0
  16. package/scripts/harness-progress-set.sh +41 -0
  17. package/scripts/harness-prompt-history.sh +164 -0
  18. package/scripts/harness-queue-manager.sh +694 -0
  19. package/scripts/harness-session-start.sh +140 -0
  20. package/scripts/harness-statusline.sh +139 -0
  21. package/scripts/harness-task-session.sh +48 -0
  22. package/scripts/harness-tmux.sh +372 -0
  23. package/scripts/harness-user-prompt-submit.sh +123 -0
  24. package/scripts/init-agents-md.sh +337 -0
  25. package/scripts/init-ref-docs.sh +262 -0
  26. package/scripts/lib/harness-audit.sh +113 -0
  27. package/scripts/lib/harness-guardrail.sh +148 -0
  28. package/scripts/lib/harness-keywait.sh +57 -0
  29. package/scripts/lib/harness-render-progress.sh +346 -0
  30. package/scripts/scan-project.sh +367 -0
  31. package/skills/brainstorming/SKILL.md +4 -4
  32. package/skills/conductor/SKILL.md +12 -16
  33. package/skills/cqo/SKILL.md +7 -0
  34. package/skills/cto/SKILL.md +7 -0
  35. package/skills/dispatcher/SKILL.md +17 -28
  36. package/skills/evaluator-code-quality/SKILL.md +1 -1
  37. package/skills/evaluator-functional/SKILL.md +1 -1
  38. package/skills/evaluator-visual/SKILL.md +1 -1
  39. package/skills/generator-backend/SKILL.md +1 -1
  40. package/skills/generator-frontend/SKILL.md +1 -1
  41. package/skills/meeting-manager/SKILL.md +33 -1
  42. package/skills/planner/SKILL.md +23 -9
  43. package/skills/service-ops/SKILL.md +19 -4
@@ -0,0 +1,393 @@
1
+ #!/bin/bash
2
+ # harness-gotcha-register.sh — Evaluator → Gotcha 자동 등록 파이프라인
3
+ #
4
+ # 사용법:
5
+ # 1) 단일 항목 등록 (수동/스크립트):
6
+ # bash harness-gotcha-register.sh <project-root> \
7
+ # --target <agent> \
8
+ # --rule-id <rule-id> \
9
+ # --title "제목" \
10
+ # --wrong "잘못된 행동" \
11
+ # --right "올바른 행동" \
12
+ # --why "근거" \
13
+ # --scope "적용 범위" \
14
+ # --source "evaluator-functional:F-003"
15
+ #
16
+ # 2) 일괄 등록 (JSON stdin/파일):
17
+ # bash harness-gotcha-register.sh <project-root> --from-json <path>
18
+ # bash harness-gotcha-register.sh <project-root> --scan-evaluations
19
+ # └ .harness/actions/evaluation-*.md 의 ```gotcha_candidates``` 블록 전부 스캔
20
+ #
21
+ # 동작:
22
+ # - 대상 파일: .harness/gotchas/<target>.md (없으면 생성)
23
+ # - 중복 감지: 동일 rule_id 가 있으면 Occurrences +1, last_seen 갱신 (본문 수정 없음)
24
+ # - 신규: 다음 G-NNN 할당, Status: unverified 로 기록
25
+ # - 등록 성공 로그: .harness/progress.log 에 한 줄 append
26
+ #
27
+ # 등록 형식:
28
+ # ### [G-NNN] <title> <!-- rule_id: <rule-id> -->
29
+ # - **Status**: unverified
30
+ # - **Date**: YYYY-MM-DD
31
+ # - **Source**: <source> (예: "evaluator-functional:F-003")
32
+ # - **Trigger**: Eval 자동 감지
33
+ # - **Wrong**: <wrong>
34
+ # - **Right**: <right>
35
+ # - **Why**: <why>
36
+ # - **Scope**: <scope>
37
+ # - **Occurrences**: 1
38
+ # - **Last-Seen**: YYYY-MM-DD
39
+
40
+ set -euo pipefail
41
+
42
+ if [ $# -lt 1 ]; then
43
+ echo "usage: harness-gotcha-register.sh <project-root> [options]" >&2
44
+ exit 1
45
+ fi
46
+
47
+ PROJECT_ROOT="$1"
48
+ shift
49
+ PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"
50
+
51
+ command -v jq >/dev/null 2>&1 || { echo "[gotcha-register] ERROR: jq required" >&2; exit 1; }
52
+
53
+ GOTCHAS_DIR="$PROJECT_ROOT/.harness/gotchas"
54
+ ACTIONS_DIR="$PROJECT_ROOT/.harness/actions"
55
+ mkdir -p "$GOTCHAS_DIR"
56
+
57
+ TODAY="$(date +%Y-%m-%d)"
58
+
59
+ # ─────────────────────────────────────────
60
+ # 단일 항목을 .harness/gotchas/<target>.md 에 append
61
+ # $1: target agent (generator-backend, generator-frontend, evaluator-functional, ...)
62
+ # $2: rule_id (dedup key)
63
+ # $3: title
64
+ # $4: wrong
65
+ # $5: right
66
+ # $6: why
67
+ # $7: scope
68
+ # $8: source
69
+ # ─────────────────────────────────────────
70
+ register_one() {
71
+ local target="$1" rule_id="$2" title="$3" wrong="$4" right="$5" why="$6" scope="$7" source="$8"
72
+ local file="$GOTCHAS_DIR/${target}.md"
73
+
74
+ # Ensure file exists with header
75
+ if [ ! -f "$file" ]; then
76
+ cat > "$file" <<EOF
77
+ # Gotchas — $target
78
+
79
+ > Dispatcher + Evaluator 가 관리. $target 은 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
80
+ > 신규 항목은 \`Status: unverified\` 로 기록되고, Planner 리뷰 후 \`verified\` 로 승격됩니다.
81
+
82
+ EOF
83
+ fi
84
+
85
+ # Dedup by rule_id marker comment
86
+ if grep -qE "<!-- rule_id: ${rule_id} -->" "$file" 2>/dev/null; then
87
+ # Bump Occurrences + Last-Seen for matching block
88
+ awk -v rid="$rule_id" -v today="$TODAY" '
89
+ BEGIN { in_block = 0 }
90
+ /^### \[G-[0-9]+\].*<!-- rule_id: / {
91
+ in_block = (index($0, "rule_id: " rid " ") > 0 || index($0, "rule_id: " rid "\n") > 0 || $0 ~ ("rule_id: " rid " -->"))
92
+ }
93
+ in_block && /^- \*\*Occurrences\*\*:/ {
94
+ n = $NF + 0
95
+ print "- **Occurrences**: " (n + 1)
96
+ next
97
+ }
98
+ in_block && /^- \*\*Last-Seen\*\*:/ {
99
+ print "- **Last-Seen**: " today
100
+ next
101
+ }
102
+ /^### / && !/<!-- rule_id: / { in_block = 0 }
103
+ { print }
104
+ ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
105
+ echo "[gotcha-register] $target: dedup $rule_id (occurrence bumped)"
106
+ return 0
107
+ fi
108
+
109
+ # Allocate next G-NNN (|| true — pipefail-safe when no existing entries)
110
+ local next_num=""
111
+ next_num=$(grep -oE '^### \[G-[0-9]+\]' "$file" 2>/dev/null | grep -oE '[0-9]+' | sort -n | tail -1 || true)
112
+ next_num=$((${next_num:-0} + 1))
113
+ local g_id
114
+ g_id=$(printf 'G-%03d' "$next_num")
115
+
116
+ # Append new block
117
+ {
118
+ echo ""
119
+ echo "### [$g_id] $title <!-- rule_id: $rule_id -->"
120
+ echo "- **Status**: unverified"
121
+ echo "- **Date**: $TODAY"
122
+ echo "- **Source**: $source"
123
+ echo "- **Trigger**: Eval 자동 감지"
124
+ echo "- **Wrong**: $wrong"
125
+ echo "- **Right**: $right"
126
+ echo "- **Why**: $why"
127
+ echo "- **Scope**: $scope"
128
+ echo "- **Occurrences**: 1"
129
+ echo "- **Last-Seen**: $TODAY"
130
+ } >> "$file"
131
+
132
+ echo "[gotcha-register] $target: registered $g_id ($rule_id) — unverified"
133
+
134
+ # Log to progress.log if present
135
+ local progress_log="$PROJECT_ROOT/.harness/progress.log"
136
+ if [ -f "$progress_log" ]; then
137
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [gotcha] $target $g_id unverified — $title (source: $source)" >> "$progress_log"
138
+ fi
139
+ }
140
+
141
+ # ─────────────────────────────────────────
142
+ # JSON 배열에서 일괄 등록
143
+ # schema: [{ target, rule_id, title, wrong, right, why, scope, source }]
144
+ # ─────────────────────────────────────────
145
+ register_from_json() {
146
+ local json="$1"
147
+ local count
148
+ count=$(echo "$json" | jq 'length' 2>/dev/null || echo 0)
149
+ if [ "$count" -eq 0 ]; then
150
+ return 0
151
+ fi
152
+
153
+ local i
154
+ for ((i=0; i<count; i++)); do
155
+ local t r ti w ri wh sc so
156
+ t=$(echo "$json" | jq -r ".[$i].target // empty")
157
+ r=$(echo "$json" | jq -r ".[$i].rule_id // empty")
158
+ ti=$(echo "$json" | jq -r ".[$i].title // empty")
159
+ w=$(echo "$json" | jq -r ".[$i].wrong // empty")
160
+ ri=$(echo "$json" | jq -r ".[$i].right // empty")
161
+ wh=$(echo "$json" | jq -r ".[$i].why // empty")
162
+ sc=$(echo "$json" | jq -r ".[$i].scope // \"항상\"")
163
+ so=$(echo "$json" | jq -r ".[$i].source // \"evaluator:auto\"")
164
+
165
+ if [ -z "$t" ] || [ -z "$r" ] || [ -z "$ti" ]; then
166
+ echo "[gotcha-register] skip: missing target/rule_id/title at index $i" >&2
167
+ continue
168
+ fi
169
+ register_one "$t" "$r" "$ti" "$w" "$ri" "$wh" "$sc" "$so"
170
+ done
171
+ }
172
+
173
+ # ─────────────────────────────────────────
174
+ # Convention 등록 — .harness/conventions/<scope>.md 에 [C-NNN] entry append
175
+ # $1: scope (shared, generator-backend, generator-frontend, ... — 파일명 base)
176
+ # $2: rule_id (dedup key)
177
+ # $3: title
178
+ # $4: rule (긍정 가이드 본문 — "X 는 항상 Y 로")
179
+ # $5: why
180
+ # $6: source
181
+ # ─────────────────────────────────────────
182
+ CONVENTIONS_DIR="$PROJECT_ROOT/.harness/conventions"
183
+ register_convention_one() {
184
+ local scope="$1" rule_id="$2" title="$3" rule="$4" why="$5" source="$6"
185
+ mkdir -p "$CONVENTIONS_DIR"
186
+ local file="$CONVENTIONS_DIR/${scope}.md"
187
+
188
+ if [ ! -f "$file" ]; then
189
+ cat > "$file" <<EOF
190
+ # Conventions — ${scope}
191
+
192
+ > Dispatcher가 관리. 긍정 가이드("~를 사용해", "항상 ~") 자동 등록.
193
+
194
+ EOF
195
+ fi
196
+
197
+ # dedup: 같은 rule_id 가 있으면 해당 [C-NNN] 블록의 Occurrences +1, Last-Seen 갱신
198
+ if grep -qE "<!-- rule_id: ${rule_id} -->" "$file" 2>/dev/null; then
199
+ awk -v rid="$rule_id" -v today="$TODAY" '
200
+ BEGIN { in_block = 0 }
201
+ /^### \[C-[0-9]+\].*<!-- rule_id: / {
202
+ in_block = ($0 ~ ("rule_id: " rid " -->"))
203
+ }
204
+ in_block && /^- \*\*Occurrences\*\*:/ {
205
+ n = $NF + 0
206
+ print "- **Occurrences**: " (n + 1)
207
+ next
208
+ }
209
+ in_block && /^- \*\*Last-Seen\*\*:/ {
210
+ print "- **Last-Seen**: " today
211
+ next
212
+ }
213
+ /^### / && !/<!-- rule_id: / { in_block = 0 }
214
+ { print }
215
+ ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"
216
+ echo "[gotcha-register] convention/${scope}: dedup ${rule_id} (occurrence bumped)"
217
+ return 0
218
+ fi
219
+
220
+ # 신규 — 다음 [C-NNN] 번호 할당 (set -e 회피: || true)
221
+ local last_n
222
+ last_n=$(grep -oE '\[C-[0-9]+\]' "$file" 2>/dev/null | grep -oE '[0-9]+' | sort -n | tail -1 || true)
223
+ last_n=${last_n:-0}
224
+ local next_n=$(printf "%03d" $((last_n + 1)))
225
+
226
+ cat >> "$file" <<EOF
227
+
228
+ ### [C-${next_n}] ${title} <!-- rule_id: ${rule_id} -->
229
+ - **Status**: unverified
230
+ - **Date**: ${TODAY}
231
+ - **Source**: ${source}
232
+ - **Rule**: ${rule}
233
+ - **Why**: ${why}
234
+ - **Occurrences**: 1
235
+ - **Last-Seen**: ${TODAY}
236
+ EOF
237
+
238
+ echo "[gotcha-register] convention/${scope}: registered C-${next_n} (${rule_id}) — unverified"
239
+ }
240
+
241
+ register_conventions_from_json() {
242
+ local json="$1"
243
+ local count
244
+ count=$(echo "$json" | jq 'length' 2>/dev/null || echo 0)
245
+ [ "$count" -eq 0 ] && return 0
246
+ local i
247
+ for ((i=0; i<count; i++)); do
248
+ local sc r ti ru wh so
249
+ sc=$(echo "$json" | jq -r ".[$i].scope // \"shared\"")
250
+ r=$(echo "$json" | jq -r ".[$i].rule_id // empty")
251
+ ti=$(echo "$json" | jq -r ".[$i].title // empty")
252
+ ru=$(echo "$json" | jq -r ".[$i].rule // empty")
253
+ wh=$(echo "$json" | jq -r ".[$i].why // empty")
254
+ so=$(echo "$json" | jq -r ".[$i].source // \"agent:auto\"")
255
+ if [ -z "$r" ] || [ -z "$ti" ] || [ -z "$ru" ]; then
256
+ echo "[gotcha-register] convention skip: missing rule_id/title/rule at index $i" >&2
257
+ continue
258
+ fi
259
+ register_convention_one "$sc" "$r" "$ti" "$ru" "$wh" "$so"
260
+ done
261
+ }
262
+
263
+ # ─────────────────────────────────────────
264
+ # 단일 파일에서 fenced block 추출 (block_tag 인자로 종류 지정)
265
+ # $1: file path · $2: block tag (gotcha_candidates 또는 convention_candidates)
266
+ # stdout 으로 각 block JSON 을 한 줄씩 출력 (\0 구분자)
267
+ # ─────────────────────────────────────────
268
+ extract_blocks() {
269
+ local file="$1" tag="$2"
270
+ local blocks_prefix
271
+ blocks_prefix=$(mktemp -d)
272
+ awk -v dir="$blocks_prefix" -v tag="$tag" '
273
+ BEGIN { idx=0; flag=0; buf="" }
274
+ $0 ~ ("^```" tag "[[:space:]]*$") { flag=1; buf=""; next }
275
+ /^```[[:space:]]*$/ && flag {
276
+ flag=0
277
+ idx++
278
+ outfile = sprintf("%s/block-%03d.json", dir, idx)
279
+ print buf > outfile
280
+ close(outfile)
281
+ buf=""
282
+ next
283
+ }
284
+ flag { buf = buf $0 "\n" }
285
+ ' "$file"
286
+ echo "$blocks_prefix"
287
+ }
288
+
289
+ process_blocks_in_file() {
290
+ local file="$1"
291
+ # gotcha_candidates
292
+ local g_dir
293
+ g_dir=$(extract_blocks "$file" "gotcha_candidates")
294
+ local b
295
+ for b in "$g_dir"/block-*.json; do
296
+ [ -f "$b" ] || continue
297
+ if jq empty "$b" 2>/dev/null; then
298
+ register_from_json "$(cat "$b")"
299
+ else
300
+ echo "[gotcha-register] skip: invalid gotcha_candidates JSON in $(basename "$file")" >&2
301
+ fi
302
+ done
303
+ rm -rf "$g_dir"
304
+
305
+ # convention_candidates
306
+ local c_dir
307
+ c_dir=$(extract_blocks "$file" "convention_candidates")
308
+ for b in "$c_dir"/block-*.json; do
309
+ [ -f "$b" ] || continue
310
+ if jq empty "$b" 2>/dev/null; then
311
+ register_conventions_from_json "$(cat "$b")"
312
+ else
313
+ echo "[gotcha-register] skip: invalid convention_candidates JSON in $(basename "$file")" >&2
314
+ fi
315
+ done
316
+ rm -rf "$c_dir"
317
+ }
318
+
319
+ # ─────────────────────────────────────────
320
+ # evaluation-*.md 스캔 — ```gotcha_candidates``` + ```convention_candidates``` 둘 다
321
+ # ─────────────────────────────────────────
322
+ scan_evaluations() {
323
+ [ -d "$ACTIONS_DIR" ] || return 0
324
+ local f
325
+ for f in "$ACTIONS_DIR"/evaluation-*.md; do
326
+ [ -f "$f" ] || continue
327
+ process_blocks_in_file "$f"
328
+ done
329
+ }
330
+
331
+ # ─────────────────────────────────────────
332
+ # 모든 worker report 스캔 — evaluation-*.md + gen-report-*.md + lead-report-*.md
333
+ # Generator 는 gen-report-{F-ID}.md 작성, Lead 는 lead-report-{date}.md 작성 가능
334
+ # ─────────────────────────────────────────
335
+ scan_all() {
336
+ [ -d "$ACTIONS_DIR" ] || return 0
337
+ local f
338
+ for f in "$ACTIONS_DIR"/evaluation-*.md "$ACTIONS_DIR"/gen-report-*.md "$ACTIONS_DIR"/lead-report-*.md; do
339
+ [ -f "$f" ] || continue
340
+ process_blocks_in_file "$f"
341
+ done
342
+ }
343
+
344
+ # ─────────────────────────────────────────
345
+ # Parse CLI
346
+ # ─────────────────────────────────────────
347
+ TARGET=""
348
+ RULE_ID=""
349
+ TITLE=""
350
+ WRONG=""
351
+ RIGHT=""
352
+ WHY=""
353
+ SCOPE="항상"
354
+ SOURCE="evaluator:auto"
355
+ MODE="single"
356
+ FROM_JSON=""
357
+
358
+ while [ $# -gt 0 ]; do
359
+ case "$1" in
360
+ --target) TARGET="$2"; shift 2 ;;
361
+ --rule-id) RULE_ID="$2"; shift 2 ;;
362
+ --title) TITLE="$2"; shift 2 ;;
363
+ --wrong) WRONG="$2"; shift 2 ;;
364
+ --right) RIGHT="$2"; shift 2 ;;
365
+ --why) WHY="$2"; shift 2 ;;
366
+ --scope) SCOPE="$2"; shift 2 ;;
367
+ --source) SOURCE="$2"; shift 2 ;;
368
+ --from-json) MODE="json"; FROM_JSON="$2"; shift 2 ;;
369
+ --scan-evaluations) MODE="scan"; shift ;;
370
+ --scan-all) MODE="scan-all"; shift ;;
371
+ *) echo "[gotcha-register] unknown arg: $1" >&2; exit 1 ;;
372
+ esac
373
+ done
374
+
375
+ case "$MODE" in
376
+ single)
377
+ if [ -z "$TARGET" ] || [ -z "$RULE_ID" ] || [ -z "$TITLE" ]; then
378
+ echo "[gotcha-register] usage: --target X --rule-id Y --title Z [...]" >&2
379
+ exit 1
380
+ fi
381
+ register_one "$TARGET" "$RULE_ID" "$TITLE" "$WRONG" "$RIGHT" "$WHY" "$SCOPE" "$SOURCE"
382
+ ;;
383
+ json)
384
+ if [ ! -f "$FROM_JSON" ]; then echo "[gotcha-register] file not found: $FROM_JSON" >&2; exit 1; fi
385
+ register_from_json "$(cat "$FROM_JSON")"
386
+ ;;
387
+ scan)
388
+ scan_evaluations
389
+ ;;
390
+ scan-all)
391
+ scan_all
392
+ ;;
393
+ esac
@@ -0,0 +1,153 @@
1
+ #!/bin/bash
2
+ # harness-meeting-doc.sh — create/read meeting decision documents
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
7
+
8
+ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || exit 1
9
+ MODE="${2:-prepare}"
10
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
11
+ MEETINGS_ROOT="$PROJECT_ROOT/.harness/actions/meetings"
12
+
13
+ command -v jq >/dev/null 2>&1 || exit 1
14
+ mkdir -p "$MEETINGS_ROOT"
15
+
16
+ requested_type=$(jq -r '.meetings.requested_type // "spec-review"' "$PROGRESS")
17
+ requested_reason=$(jq -r '.meetings.requested_reason // "unspecified"' "$PROGRESS")
18
+ drift=$(jq -r '.service_ops.drift_classification // .meetings.decision.drift_classification // "unknown"' "$PROGRESS")
19
+ goal_adherence=$(jq -r '.goals.current_adherence // "unknown"' "$PROGRESS")
20
+ ops_report=$(jq -r '.service_ops.auto_retro.last_report // empty' "$PROGRESS")
21
+ cqo_audit=$(jq -r '.cqo.audit_path // empty' "$PROGRESS")
22
+ cto_review=$(jq -r '.cto.review_path // empty' "$PROGRESS")
23
+ current_path=$(jq -r '.meetings.current_record_path // empty' "$PROGRESS")
24
+
25
+ default_decision_json() {
26
+ local owner="planner"
27
+ local action_type="triage"
28
+ case "$1" in
29
+ implementation_drift) owner="cto"; action_type="implement" ;;
30
+ planning_drift) owner="planner"; action_type="replan" ;;
31
+ ops_drift) owner="service-ops"; action_type="monitor" ;;
32
+ goal_drift) owner="dispatcher"; action_type="escalate-owner" ;;
33
+ esac
34
+ if [ "$requested_reason" = "goal-intake" ]; then
35
+ owner="planner"
36
+ action_type="goal-alignment"
37
+ fi
38
+ jq -n \
39
+ --arg owner "$owner" \
40
+ --arg action_type "$action_type" \
41
+ --arg rationale "initial skeleton created by meeting-manager; replace with evidence-backed conclusion" \
42
+ --arg drift_classification "$1" \
43
+ --arg ops_report "$ops_report" \
44
+ --arg cqo_audit "$cqo_audit" \
45
+ --arg cto_review "$cto_review" '
46
+ {
47
+ owner: $owner,
48
+ action_type: $action_type,
49
+ rationale: $rationale,
50
+ evidence: (
51
+ [
52
+ (if $ops_report != "" then {source:$ops_report, kind:"ops-report"} else empty end),
53
+ (if $cqo_audit != "" then {source:$cqo_audit, kind:"cqo-audit"} else empty end),
54
+ (if $cto_review != "" then {source:$cto_review, kind:"cto-review"} else empty end)
55
+ ]
56
+ ),
57
+ drift_classification: $drift_classification
58
+ }'
59
+ }
60
+
61
+ if [ "$MODE" = "prepare" ]; then
62
+ meeting_id="M-$(date -u +%Y%m%dT%H%M%SZ)"
63
+ meeting_dir="$MEETINGS_ROOT/$meeting_id"
64
+ mkdir -p "$meeting_dir"
65
+ record="$meeting_dir/meeting-$meeting_id.md"
66
+
67
+ cat > "$meeting_dir/notice.md" <<EOF
68
+ # Notice — $meeting_id
69
+
70
+ - type: $requested_type
71
+ - reason: $requested_reason
72
+ - goal_adherence: $goal_adherence
73
+
74
+ ## Request
75
+ "$requested_reason" 요청이 접수되었습니다. 참석자들은 각자의 전문 시각에서 사실·증거를 제출하고,
76
+ 다음 owner와 action_type을 evidence 중심으로 결정하십시오.
77
+ EOF
78
+
79
+ cat > "$meeting_dir/prep-dispatcher.md" <<'EOF'
80
+ # Prep — Dispatcher/CEO
81
+ - Goal 자체가 흔들렸는가?
82
+ - Owner escalation 이 필요한가?
83
+ - 사업 우선순위 또는 KPI 정의를 바꿔야 하는가?
84
+ EOF
85
+ cat > "$meeting_dir/prep-planner.md" <<'EOF'
86
+ # Prep — Planner/COO
87
+ - 기획/가설/웹리서치/레퍼런스 보강이 필요한가?
88
+ - planning_drift 또는 goal_drift 근거는 무엇인가?
89
+ - 수정해야 할 plan/feature/api 는 무엇인가?
90
+ EOF
91
+ cat > "$meeting_dir/prep-cto.md" <<'EOF'
92
+ # Prep — CTO
93
+ - 구현/아키텍처/기술선택이 원인인가?
94
+ - implementation_drift 근거는 무엇인가?
95
+ - 어떤 generator/evaluator를 다시 태워야 하는가?
96
+ EOF
97
+ cat > "$meeting_dir/prep-cqo.md" <<'EOF'
98
+ # Prep — CQO
99
+ - 품질/회귀/검증 부족이 원인인가?
100
+ - 어떤 evidence가 이 결론을 지지하는가?
101
+ - evidence 없는 추정은 금지한다.
102
+ EOF
103
+ cat > "$meeting_dir/prep-service-ops.md" <<'EOF'
104
+ # Prep — Service-Ops
105
+ - 어떤 KPI/로그/incident가 Goal에서 벗어났는가?
106
+ - ops_drift 여부를 먼저 판단하라.
107
+ - 운영 측 evidence를 문서 경로와 함께 적어라.
108
+ EOF
109
+
110
+ decision_json="$(default_decision_json "$drift")"
111
+ cat > "$record" <<EOF
112
+ # Meeting Record — $meeting_id
113
+
114
+ ## Context
115
+ - type: $requested_type
116
+ - reason: $requested_reason
117
+ - goal_adherence: $goal_adherence
118
+
119
+ ## Decision JSON
120
+ \`\`\`json
121
+ $(jq -n \
122
+ --arg meeting_id "$meeting_id" \
123
+ --arg type "$requested_type" \
124
+ --arg reason "$requested_reason" \
125
+ --arg source_path "${record#$PROJECT_ROOT/}" \
126
+ --argjson decision "$decision_json" \
127
+ '{meeting_id:$meeting_id,type:$type,reason:$reason,decision:($decision + {source_path:$source_path})}')
128
+ \`\`\`
129
+ EOF
130
+
131
+ bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" \
132
+ ".meetings.current_id = \"$meeting_id\" |
133
+ .meetings.current_record_path = \"${record#$PROJECT_ROOT/}\" |
134
+ .meetings.decision = ($(jq -c '.decision' <<<"$(
135
+ jq -n --arg meeting_id "$meeting_id" --arg type "$requested_type" --arg reason "$requested_reason" --arg source_path "${record#$PROJECT_ROOT/}" --argjson decision "$decision_json" '{meeting_id:$meeting_id,type:$type,reason:$reason,decision:($decision + {source_path:$source_path})}'
136
+ )"))" >/dev/null
137
+
138
+ echo "${record#$PROJECT_ROOT/}"
139
+ exit 0
140
+ fi
141
+
142
+ if [ "$MODE" = "read-decision" ]; then
143
+ [ -n "$current_path" ] || exit 0
144
+ [ -f "$PROJECT_ROOT/$current_path" ] || exit 0
145
+ awk '
146
+ /```json/ {capture=1; next}
147
+ /```/ && capture {capture=0; exit}
148
+ capture {print}
149
+ ' "$PROJECT_ROOT/$current_path" | jq -c '.decision'
150
+ exit 0
151
+ fi
152
+
153
+ exit 2