@walwal-harness/cli 6.0.4 → 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.
@@ -0,0 +1,382 @@
1
+ #!/bin/bash
2
+ # conductor-tick.sh — document-driven company-loop router
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 0
9
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
10
+ CONFIG="$PROJECT_ROOT/.harness/config.json"
11
+ FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
12
+
13
+ [ -f "$PROGRESS" ] || exit 0
14
+ [ -f "$CONFIG" ] || exit 0
15
+ command -v jq >/dev/null 2>&1 || exit 0
16
+
17
+ candidate="${HARNESS_CONDUCTOR_CANDIDATE:-}"
18
+ if [ -z "$candidate" ]; then
19
+ candidate=$(jq -r '.next_agent // "null"' "$PROGRESS" 2>/dev/null || echo "null")
20
+ fi
21
+
22
+ current_agent=$(jq -r '.current_agent // "null"' "$PROGRESS")
23
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
24
+ pipeline=$(jq -r '.pipeline // "null"' "$PROGRESS")
25
+ mode=$(jq -r '.mode // "auto"' "$PROGRESS")
26
+ user_override=$(jq -r '.mode_decision.user_override // "null"' "$PROGRESS")
27
+ requested_mode=$(jq -r '.service_ops.requested_mode // "null"' "$PROGRESS")
28
+ goal_adherence=$(jq -r '.goals.current_adherence // "null"' "$PROGRESS")
29
+ meetings_active_count=$(jq -r '(.meetings.active | length) // 0' "$PROGRESS")
30
+ cqo_verdict=$(jq -r '.cqo.sprint_verdict // "pending"' "$PROGRESS")
31
+ ops_report=$(jq -r '.service_ops.auto_retro.last_report // "null"' "$PROGRESS")
32
+ cto_hotfixes=$(jq -r '.cto.open_hotfixes // 0' "$PROGRESS")
33
+ ops_recommendations=$(jq -r '.service_ops.auto_retro.open_recommendations // 0' "$PROGRESS")
34
+ ops_alerts=$(jq -r '.service_ops.monitor.alerts_this_sprint // 0' "$PROGRESS")
35
+ open_incidents=$(jq -r '(.service_ops.incident.open | length) // 0' "$PROGRESS")
36
+ workflow_stage=$(jq -r '.workflow.stage // "goal-intake"' "$PROGRESS")
37
+ meeting_type=$(jq -r '.meetings.requested_type // "null"' "$PROGRESS")
38
+ meeting_reason=$(jq -r '.meetings.requested_reason // "null"' "$PROGRESS")
39
+ cqo_regressions=$(jq -r '.cqo.open_regressions // 0' "$PROGRESS")
40
+ cqo_axes_below=$(jq -r '(.cqo.axes_below_threshold | length) // 0' "$PROGRESS")
41
+ plan_status=$(jq -r '.artifacts["plan.md"].status // "pending"' "$PROGRESS")
42
+ feature_status=$(jq -r '.artifacts["feature-list.json"].status // "pending"' "$PROGRESS")
43
+ api_status=$(jq -r '.artifacts["api-contract.json"].status // "pending"' "$PROGRESS")
44
+ service_ops_drift=$(jq -r '.service_ops.drift_classification // "null"' "$PROGRESS")
45
+
46
+ set_progress() {
47
+ bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" "$1" >/dev/null
48
+ }
49
+
50
+ escape_json_string() {
51
+ jq -Rn --arg v "$1" '$v'
52
+ }
53
+
54
+ maybe_select_mode() {
55
+ local effective="$mode"
56
+ if [ "$user_override" != "null" ] && [ -n "$user_override" ]; then
57
+ effective="$user_override"
58
+ set_progress ".mode = \"$effective\" |
59
+ .mode_decision.owner = \"conductor\" |
60
+ .mode_decision.user_override = \"$effective\" |
61
+ .mode_decision.decided_at = (now | todate) |
62
+ .mode_decision.rationale = \"user override: $effective\""
63
+ echo "$effective"
64
+ return
65
+ fi
66
+ if [ "$mode" != "auto" ] || [ ! -f "$FEATURES" ]; then
67
+ echo "$mode"
68
+ return
69
+ fi
70
+
71
+ local feature_count ready_at_start critical_depth
72
+ feature_count=$(jq '.features | length // 0' "$FEATURES" 2>/dev/null || echo 0)
73
+ ready_at_start=$(jq '[.features[]? | select(((.depends_on // []) | length) == 0)] | length' "$FEATURES" 2>/dev/null || echo 0)
74
+ critical_depth=$(jq '
75
+ def feature($id): .features[]? | select(.id == $id);
76
+ def depth($id):
77
+ (feature($id) | .depends_on // []) as $deps
78
+ | if ($deps | length) == 0 then 1 else ([ $deps[] | depth(.) ] | max + 1) end;
79
+ ([.features[]?.id | depth(.)] | max) // 0
80
+ ' "$FEATURES" 2>/dev/null || echo 0)
81
+
82
+ local team_ready team_features team_depth selected rationale
83
+ team_ready=$(jq -r '.mode_selection.rules.force_team_when.ready_at_start_gte // 3' "$CONFIG")
84
+ team_features=$(jq -r '.mode_selection.rules.force_team_when.feature_count_gte // 6' "$CONFIG")
85
+ team_depth=$(jq -r '.mode_selection.rules.force_team_when.critical_path_depth_lte // 2' "$CONFIG")
86
+
87
+ if [ "$ready_at_start" -ge "$team_ready" ] && [ "$feature_count" -ge "$team_features" ] && [ "$critical_depth" -le "$team_depth" ]; then
88
+ selected="team"
89
+ else
90
+ selected=$(jq -r '.mode_selection.rules.tie_breaker // "team"' "$CONFIG")
91
+ fi
92
+ rationale="company-default: ready=${ready_at_start}, features=${feature_count}, depth=${critical_depth} -> ${selected}"
93
+ set_progress ".mode = \"$selected\" |
94
+ .mode_decision.owner = \"conductor\" |
95
+ .mode_decision.decided_at = (now | todate) |
96
+ .mode_decision.rationale = \"$rationale\""
97
+ echo "$selected"
98
+ }
99
+
100
+ infer_drift_classification() {
101
+ if [ "$service_ops_drift" != "null" ] && [ -n "$service_ops_drift" ]; then
102
+ echo "$service_ops_drift"
103
+ return
104
+ fi
105
+ if [ "$cqo_verdict" = "FAIL" ] || [ "${cqo_regressions:-0}" -gt 0 ] || [ "${cqo_axes_below:-0}" -gt 0 ]; then
106
+ echo "implementation_drift"
107
+ return
108
+ fi
109
+ if [ "$requested_mode" = "incident" ] || [ "${open_incidents:-0}" -gt 0 ] || [ "${ops_alerts:-0}" -gt 0 ]; then
110
+ echo "ops_drift"
111
+ return
112
+ fi
113
+ if [ "$goal_adherence" != "null" ] && awk "BEGIN {exit !($goal_adherence < 0.7)}"; then
114
+ if [ "$cqo_verdict" = "PASS" ] && [ "${open_incidents:-0}" -eq 0 ] && [ "${ops_alerts:-0}" -eq 0 ]; then
115
+ echo "planning_drift"
116
+ elif [ "$(jq -r '.goals.active_id // "null"' "$PROGRESS")" = "null" ]; then
117
+ echo "goal_drift"
118
+ else
119
+ echo "goal_drift"
120
+ fi
121
+ return
122
+ fi
123
+ echo "unknown"
124
+ }
125
+
126
+ meeting_stage_for_owner() {
127
+ case "$1" in
128
+ planner) echo "coo-planning" ;;
129
+ cto) echo "cto-review" ;;
130
+ cqo) echo "quality-review" ;;
131
+ service-ops) echo "ops-review" ;;
132
+ dispatcher) echo "goal-intake" ;;
133
+ *) echo "goal-intake" ;;
134
+ esac
135
+ }
136
+
137
+ load_meeting_decision() {
138
+ local decision
139
+ decision=$(bash "$SCRIPT_DIR/harness-meeting-doc.sh" "$PROJECT_ROOT" read-decision 2>/dev/null || true)
140
+ if [ -n "$decision" ]; then
141
+ echo "$decision"
142
+ else
143
+ jq -c '.meetings.decision // {}' "$PROGRESS"
144
+ fi
145
+ }
146
+
147
+ effective_mode="$(maybe_select_mode)"
148
+ drift_classification="$(infer_drift_classification)"
149
+
150
+ next="$candidate"
151
+ action="pass-through"
152
+ new_conductor_state="running"
153
+ new_workflow_stage="$workflow_stage"
154
+ meeting_filter=""
155
+ planner_filter=""
156
+ ops_filter=""
157
+ meeting_prepare=false
158
+
159
+ plan_artifacts_ready=false
160
+ if [ "$plan_status" != "pending" ] || [ "$feature_status" != "pending" ] || [ "$api_status" != "pending" ]; then
161
+ plan_artifacts_ready=true
162
+ fi
163
+
164
+ first_generator="generator-backend"
165
+ if [ "$pipeline" = "FE-ONLY" ]; then
166
+ first_generator="generator-frontend"
167
+ fi
168
+
169
+ if [ "$current_agent" = "dispatcher" ] && [ "$agent_status" = "completed" ]; then
170
+ next="meeting-manager"
171
+ action="convene:all-hands:goal-intake"
172
+ new_conductor_state="waiting_meeting"
173
+ new_workflow_stage="ceo-meeting"
174
+ meeting_prepare=true
175
+ meeting_filter="
176
+ .meetings.active = [\"meeting-manager\"] |
177
+ .meetings.requested_type = \"all-hands\" |
178
+ .meetings.requested_reason = \"goal-intake\" |
179
+ .meetings.decision = {
180
+ \"owner\": \"planner\",
181
+ \"action_type\": \"goal-alignment\",
182
+ \"rationale\": \"CEO intake always starts with COO alignment.\",
183
+ \"evidence\": [],
184
+ \"drift_classification\": null,
185
+ \"source_path\": null
186
+ }"
187
+
188
+ elif [ "$meetings_active_count" -gt 0 ] && [ "$current_agent" != "meeting-manager" ]; then
189
+ next="meeting-manager"
190
+ action="spawn:meeting-manager:${meeting_type}"
191
+ new_conductor_state="waiting_meeting"
192
+
193
+ elif [ "$requested_mode" != "null" ] && [ -n "$requested_mode" ] && [ "$current_agent" != "service-ops" ]; then
194
+ next="service-ops"
195
+ action="spawn:service-ops:${requested_mode}"
196
+
197
+ elif [ "$goal_adherence" != "null" ] && awk "BEGIN {exit !($goal_adherence < 0.7)}"; then
198
+ next="meeting-manager"
199
+ action="convene:spec-review:${drift_classification}"
200
+ new_conductor_state="waiting_meeting"
201
+ new_workflow_stage="goal-drift-review"
202
+ meeting_prepare=true
203
+ meeting_filter="
204
+ .meetings.active = [\"meeting-manager\"] |
205
+ .meetings.requested_type = \"spec-review\" |
206
+ .meetings.requested_reason = \"goal-drift\" |
207
+ .service_ops.drift_classification = \"$drift_classification\" |
208
+ .meetings.decision = {
209
+ \"owner\": null,
210
+ \"action_type\": null,
211
+ \"rationale\": null,
212
+ \"evidence\": [],
213
+ \"drift_classification\": \"$drift_classification\",
214
+ \"source_path\": null
215
+ }"
216
+
217
+ elif [ "$current_agent" = "meeting-manager" ] && [ "$agent_status" = "completed" ]; then
218
+ meeting_decision="$(load_meeting_decision)"
219
+ owner=$(jq -r '.owner // "null"' <<<"$meeting_decision")
220
+ action_type=$(jq -r '.action_type // "triage"' <<<"$meeting_decision")
221
+ rationale=$(jq -r '.rationale // ""' <<<"$meeting_decision")
222
+ evidence=$(jq -c '.evidence // []' <<<"$meeting_decision")
223
+ decision_drift=$(jq -r '.drift_classification // "unknown"' <<<"$meeting_decision")
224
+ source_path=$(jq -r '.source_path // "null"' <<<"$meeting_decision")
225
+
226
+ if [ "$owner" = "null" ] || [ -z "$owner" ]; then
227
+ case "$decision_drift" in
228
+ implementation_drift) owner="cto" ;;
229
+ planning_drift) owner="planner" ;;
230
+ ops_drift) owner="service-ops" ;;
231
+ goal_drift) owner="dispatcher" ;;
232
+ *)
233
+ if [ "$plan_artifacts_ready" = false ]; then owner="planner"; else owner="cto"; fi
234
+ ;;
235
+ esac
236
+ fi
237
+
238
+ next="$owner"
239
+ action="dispatch:${owner}:${action_type}"
240
+ new_workflow_stage="$(meeting_stage_for_owner "$owner")"
241
+ if [ "$owner" = "planner" ]; then
242
+ planner_filter=".planner.requested_mode = \"$action_type\""
243
+ fi
244
+ meeting_filter="
245
+ .meetings.active = [] |
246
+ .meetings.last_type = .meetings.requested_type |
247
+ .meetings.last_reason = .meetings.requested_reason |
248
+ .meetings.last_decision = \"$owner:$action_type\" |
249
+ .meetings.requested_type = null |
250
+ .meetings.requested_reason = null |
251
+ .meetings.decision = {
252
+ \"owner\": \"$owner\",
253
+ \"action_type\": \"$action_type\",
254
+ \"rationale\": $(escape_json_string "$rationale"),
255
+ \"evidence\": $evidence,
256
+ \"drift_classification\": $(escape_json_string "$decision_drift"),
257
+ \"source_path\": $(escape_json_string "$source_path")
258
+ }"
259
+
260
+ elif [ "$current_agent" = "planner" ] && [ "$agent_status" = "completed" ]; then
261
+ next="cto"
262
+ action="handoff:cto:plan-ready"
263
+ new_workflow_stage="cto-review"
264
+ planner_filter='
265
+ .planner.last_brief = (.planner.requested_mode // "goal-alignment") |
266
+ .planner.requested_mode = null'
267
+
268
+ elif [ "$current_agent" = "cto" ]; then
269
+ if [ "${cto_hotfixes:-0}" -gt 0 ] || [ "${ops_recommendations:-0}" -gt 0 ]; then
270
+ next="planner"
271
+ action="dispatch:planner:execution-plan"
272
+ new_workflow_stage="coo-replan"
273
+ planner_filter='.planner.requested_mode = "execution-plan"'
274
+ elif [ "$candidate" = "archive" ] && [ "$workflow_stage" = "ops-review" ]; then
275
+ next="archive"
276
+ action="archive"
277
+ new_conductor_state="completed"
278
+ new_workflow_stage="ops-monitoring"
279
+ else
280
+ next="$first_generator"
281
+ action="dispatch:workers:${first_generator}"
282
+ new_workflow_stage="implementation"
283
+ fi
284
+
285
+ elif [ "$candidate" = "archive" ]; then
286
+ if [[ "$current_agent" == evaluator-* ]] && [ "$cqo_verdict" = "pending" ]; then
287
+ next="cqo"
288
+ action="spawn:cqo"
289
+ new_workflow_stage="quality-review"
290
+ elif [ "$current_agent" = "cqo" ]; then
291
+ if [ "$cqo_verdict" = "FAIL" ] || [ "${cqo_regressions:-0}" -gt 0 ] || [ "${cqo_axes_below:-0}" -gt 0 ]; then
292
+ next="meeting-manager"
293
+ action="convene:spec-review:implementation_drift"
294
+ new_conductor_state="waiting_meeting"
295
+ new_workflow_stage="quality-review"
296
+ meeting_prepare=true
297
+ meeting_filter='
298
+ .meetings.active = ["meeting-manager"] |
299
+ .meetings.requested_type = "spec-review" |
300
+ .meetings.requested_reason = "quality-fail" |
301
+ .service_ops.drift_classification = "implementation_drift" |
302
+ .meetings.decision = {
303
+ "owner": null,
304
+ "action_type": null,
305
+ "rationale": null,
306
+ "evidence": [],
307
+ "drift_classification": "implementation_drift",
308
+ "source_path": null
309
+ }'
310
+ else
311
+ next="service-ops"
312
+ action="spawn:service-ops:auto-retro"
313
+ new_workflow_stage="ops-review"
314
+ ops_filter='.service_ops.requested_mode = "auto-retro"'
315
+ fi
316
+ elif [ "$cqo_verdict" != "pending" ] && [ "$ops_report" = "null" ]; then
317
+ next="service-ops"
318
+ action="spawn:service-ops:auto-retro"
319
+ new_workflow_stage="ops-review"
320
+ ops_filter='.service_ops.requested_mode = "auto-retro"'
321
+ elif [ "$current_agent" = "service-ops" ]; then
322
+ local_drift="$drift_classification"
323
+ 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
324
+ next="meeting-manager"
325
+ action="convene:sprint-review:${local_drift}"
326
+ new_conductor_state="waiting_meeting"
327
+ new_workflow_stage="ops-review"
328
+ meeting_prepare=true
329
+ meeting_filter="
330
+ .meetings.active = [\"meeting-manager\"] |
331
+ .meetings.requested_type = \"sprint-review\" |
332
+ .meetings.requested_reason = \"ops-batch\" |
333
+ .service_ops.drift_classification = \"$local_drift\" |
334
+ .meetings.decision = {
335
+ \"owner\": null,
336
+ \"action_type\": null,
337
+ \"rationale\": null,
338
+ \"evidence\": [],
339
+ \"drift_classification\": \"$local_drift\",
340
+ \"source_path\": null
341
+ }"
342
+ else
343
+ next="archive"
344
+ action="archive"
345
+ new_conductor_state="completed"
346
+ new_workflow_stage="ops-monitoring"
347
+ ops_filter='.service_ops.requested_mode = null'
348
+ fi
349
+ fi
350
+ fi
351
+
352
+ filter=".conductor.state = \"$new_conductor_state\" |
353
+ .conductor.current_action = \"$action\" |
354
+ .conductor.last_tick = (now | todate) |
355
+ .conductor.tick_count = ((.conductor.tick_count // 0) + 1) |
356
+ .next_agent = \"$next\" |
357
+ .workflow.stage = \"$new_workflow_stage\" |
358
+ .workflow.last_transition = (now | todate) |
359
+ .workflow.last_reason = \"$action\""
360
+
361
+ if [ -n "$meeting_filter" ]; then
362
+ filter="$filter | $meeting_filter"
363
+ fi
364
+ if [ -n "$planner_filter" ]; then
365
+ filter="$filter | $planner_filter"
366
+ fi
367
+ if [ -n "$ops_filter" ]; then
368
+ filter="$filter | $ops_filter"
369
+ elif [ "$current_agent" = "service-ops" ] && [ "$next" != "service-ops" ]; then
370
+ filter="$filter | .service_ops.requested_mode = null"
371
+ fi
372
+ if [ "$next" = "meeting-manager" ] && [ "$workflow_stage" = "ops-review" ]; then
373
+ filter="$filter | .workflow.loop_count = ((.workflow.loop_count // 0) + 1)"
374
+ fi
375
+
376
+ set_progress "$filter"
377
+
378
+ if [ "$meeting_prepare" = true ] && [ "$next" = "meeting-manager" ]; then
379
+ bash "$SCRIPT_DIR/harness-meeting-doc.sh" "$PROJECT_ROOT" prepare >/dev/null 2>&1 || true
380
+ fi
381
+
382
+ echo "$next"
@@ -89,7 +89,21 @@ jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
89
89
  | .agent_status = "pending"
90
90
  | .completed_agents = []
91
91
  | .next_agent = "dispatcher"
92
+ | .workflow = { stage: "goal-intake", last_transition: null, loop_count: 0, last_reason: null }
93
+ | .task_sessions.current = { agent: null, id: null, path: null, source: null }
92
94
  | .failure = { agent: null, location: null, message: null, retry_target: null }
95
+ | .meetings.active = []
96
+ | .meetings.current_id = null
97
+ | .meetings.current_record_path = null
98
+ | .meetings.requested_type = null
99
+ | .meetings.requested_reason = null
100
+ | .meetings.last_type = null
101
+ | .meetings.last_reason = null
102
+ | .meetings.last_decision = null
103
+ | .meetings.decision = { owner: null, action_type: null, rationale: null, evidence: [], drift_classification: null, source_path: null }
104
+ | .service_ops.requested_mode = null
105
+ | .service_ops.drift_classification = null
106
+ | .service_ops.evidence = []
93
107
  | (.artifacts // {}) as $a
94
108
  | .artifacts = ($a | with_entries(
95
109
  if (.value | type) == "object"
@@ -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
@@ -189,6 +189,21 @@ run_pre_eval_gate() {
189
189
  fi
190
190
  }
191
191
 
192
+ # ─────────────────────────────────────────
193
+ # Conductor Tick — lightweight 6.0 routing bridge
194
+ # next_agent 계산 후, Company loop가 개입해야 하면 next_agent를 재작성.
195
+ # ─────────────────────────────────────────
196
+ run_conductor_tick() {
197
+ local candidate="$1"
198
+ local owner
199
+ owner=$(jq -r '.mode_selection.owner // empty' "$CONFIG" 2>/dev/null || true)
200
+ if [ "$owner" != "conductor" ]; then return 0; fi
201
+ if [ ! -x "$SCRIPT_DIR/conductor-tick.sh" ]; then return 0; fi
202
+
203
+ HARNESS_CONDUCTOR_CANDIDATE="$candidate" bash "$SCRIPT_DIR/conductor-tick.sh" "$PROJECT_ROOT" >/dev/null 2>&1 || true
204
+ next_agent=$(jq -r '.next_agent // "null"' "$PROGRESS" 2>/dev/null || echo "$candidate")
205
+ }
206
+
192
207
  # ─────────────────────────────────────────
193
208
  # Determine next agent
194
209
  # ─────────────────────────────────────────
@@ -350,6 +365,13 @@ if [ "$agent_status" = "completed" ]; then
350
365
  fi
351
366
  fi
352
367
 
368
+ # ─────────────────────────────────────────
369
+ # Company routing bridge — handoff 직전 conductor 가 next_agent 조정
370
+ # ─────────────────────────────────────────
371
+ if [ "$next_agent" != "null" ] && [ "$agent_status" != "blocked" ]; then
372
+ run_conductor_tick "$next_agent"
373
+ fi
374
+
353
375
  # ─────────────────────────────────────────
354
376
  # Render progress
355
377
  # ─────────────────────────────────────────
@@ -376,6 +398,10 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
376
398
  # ── Read model & thinking mode for next agent ──
377
399
  agent_model=$(jq -r ".agents[\"${next_agent}\"].model // \"opus\"" "$CONFIG" 2>/dev/null || echo "opus")
378
400
  agent_thinking=$(jq -r ".agents[\"${next_agent}\"].thinking_mode // \"null\"" "$CONFIG" 2>/dev/null || echo "null")
401
+ task_session_path=""
402
+ if [ -x "$SCRIPT_DIR/harness-task-session.sh" ]; then
403
+ task_session_path=$(bash "$SCRIPT_DIR/harness-task-session.sh" "$PROJECT_ROOT" "$next_agent" "${current_agent:-dispatcher}" 2>/dev/null || true)
404
+ fi
379
405
 
380
406
  # ── Build prompt text (embedded in handoff.json) ──
381
407
  prompt="/harness-${next_agent} 를 실행하세요."
@@ -389,6 +415,9 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
389
415
  fi
390
416
 
391
417
  prompt+=$'\n'".harness/handoff.json을 읽고 컨텍스트를 확인하세요."
418
+ if [ -n "$task_session_path" ]; then
419
+ prompt+=$'\n'"${task_session_path} 를 현재 task session의 단일 기준 문서로 사용하세요."
420
+ fi
392
421
 
393
422
  # Inject thinking mode instruction
394
423
  if [ "$agent_thinking" != "null" ]; then
@@ -492,6 +521,7 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
492
521
  --argjson eval_config "$eval_config" \
493
522
  --argjson cross_val "$cross_validation_data" \
494
523
  --argjson cross_val_cq "$cross_validation_from_code_quality" \
524
+ --arg task_session_path "$task_session_path" \
495
525
  --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
496
526
  '{
497
527
  from: $from,
@@ -509,6 +539,7 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
509
539
  eval_config: $eval_config,
510
540
  cross_validation_from_functional: $cross_val,
511
541
  cross_validation_from_code_quality: $cross_val_cq,
542
+ task_session_path: (if $task_session_path == "" then null else $task_session_path end),
512
543
  warnings: [],
513
544
  timestamp: $timestamp
514
545
  }' > "$HANDOFF"