@walwal-harness/cli 6.1.2 → 6.1.4

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 (54) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +4 -2
  3. package/assets/templates/HARNESS.md +216 -403
  4. package/assets/templates/config.json +3 -3
  5. package/conventions/README.md +92 -0
  6. package/conventions/conductor.md +24 -0
  7. package/conventions/coo-developer.md +24 -0
  8. package/conventions/cqo.md +24 -0
  9. package/conventions/cto.md +24 -0
  10. package/conventions/dispatcher.md +24 -0
  11. package/conventions/documentationer.md +24 -0
  12. package/conventions/evaluator-architecture.md +24 -0
  13. package/conventions/evaluator-code-quality.md +24 -0
  14. package/conventions/evaluator-functional.md +24 -0
  15. package/conventions/evaluator-security.md +24 -0
  16. package/conventions/evaluator-visual.md +24 -0
  17. package/conventions/generator-backend.md +24 -0
  18. package/conventions/generator-designer.md +24 -0
  19. package/conventions/generator-devops.md +24 -0
  20. package/conventions/generator-frontend.md +24 -0
  21. package/conventions/meeting-manager.md +24 -0
  22. package/conventions/planner.md +24 -0
  23. package/conventions/service-ops.md +24 -0
  24. package/conventions/shared.md +24 -0
  25. package/gotchas/README.md +40 -8
  26. package/gotchas/conductor.md +12 -82
  27. package/gotchas/coo-developer.md +21 -12
  28. package/gotchas/cqo.md +22 -0
  29. package/gotchas/cto.md +22 -0
  30. package/gotchas/dispatcher.md +12 -87
  31. package/gotchas/documentationer.md +21 -12
  32. package/gotchas/evaluator-architecture.md +22 -0
  33. package/gotchas/evaluator-code-quality.md +11 -20
  34. package/gotchas/evaluator-functional.md +10 -30
  35. package/gotchas/evaluator-security.md +22 -0
  36. package/gotchas/evaluator-visual.md +10 -19
  37. package/gotchas/generator-backend-laravel.md +11 -77
  38. package/gotchas/generator-backend.md +19 -2
  39. package/gotchas/generator-designer.md +22 -0
  40. package/gotchas/generator-devops.md +22 -0
  41. package/gotchas/generator-frontend.md +13 -22
  42. package/gotchas/meeting-manager.md +22 -0
  43. package/gotchas/planner.md +19 -2
  44. package/gotchas/service-ops.md +13 -18
  45. package/package.json +2 -1
  46. package/scripts/conductor-tick.sh +167 -21
  47. package/scripts/harness-meeting-doc.sh +146 -2
  48. package/scripts/harness-session-start.sh +7 -0
  49. package/scripts/lib/harness-progress-migrate.sh +58 -0
  50. package/skills/conductor/SKILL.md +1 -0
  51. package/skills/coo-developer/SKILL.md +35 -5
  52. package/skills/documentationer/SKILL.md +55 -9
  53. package/skills/meeting-manager/SKILL.md +114 -4
  54. package/skills/planner/SKILL.md +45 -0
@@ -0,0 +1,22 @@
1
+ ---
2
+ docmeta:
3
+ id: gotchas-generator-designer
4
+ title: Gotchas — ugenerator-udesigner
5
+ type: input
6
+ createdAt: 2026-05-08T00:00:00Z
7
+ updatedAt: 2026-05-08T00:00:00Z
8
+ source:
9
+ producer: user
10
+ skillId: harness-dispatcher
11
+ inputs: []
12
+ tags: [gotchas, generator-designer]
13
+ ---
14
+
15
+ # Gotchas — ugenerator-udesigner
16
+
17
+ > Dispatcher 가 관리. ugenerator-udesigner 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
18
+ >
19
+ > 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
20
+ > `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
21
+
22
+ <!-- 항목이 추가되면 아래에 기록됩니다 -->
@@ -0,0 +1,22 @@
1
+ ---
2
+ docmeta:
3
+ id: gotchas-generator-devops
4
+ title: Gotchas — ugenerator-udevops
5
+ type: input
6
+ createdAt: 2026-05-08T00:00:00Z
7
+ updatedAt: 2026-05-08T00:00:00Z
8
+ source:
9
+ producer: user
10
+ skillId: harness-dispatcher
11
+ inputs: []
12
+ tags: [gotchas, generator-devops]
13
+ ---
14
+
15
+ # Gotchas — ugenerator-udevops
16
+
17
+ > Dispatcher 가 관리. ugenerator-udevops 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
18
+ >
19
+ > 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
20
+ > `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
21
+
22
+ <!-- 항목이 추가되면 아래에 기록됩니다 -->
@@ -1,31 +1,22 @@
1
1
  ---
2
2
  docmeta:
3
- id: generator-frontend
4
- title: Gotchas — Generator-Frontend
5
- type: intermediate
6
- createdAt: 2026-05-07T00:00:00Z
7
- updatedAt: 2026-05-07T00:00:00Z
3
+ id: gotchas-generator-frontend
4
+ title: Gotchas — ugenerator-ufrontend
5
+ type: input
6
+ createdAt: 2026-05-08T00:00:00Z
7
+ updatedAt: 2026-05-08T00:00:00Z
8
8
  source:
9
- producer: agent
9
+ producer: user
10
10
  skillId: harness-dispatcher
11
11
  inputs: []
12
- tags: [gotcha, generator-frontend, sub-skill, agency-agents]
12
+ tags: [gotchas, generator-frontend]
13
13
  ---
14
14
 
15
- # Gotchas — Generator-Frontend
15
+ # Gotchas — ugenerator-ufrontend
16
16
 
17
- > Dispatcher가 관리. Generator-Frontend는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
17
+ > Dispatcher 가 관리. ugenerator-ufrontend 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
18
+ >
19
+ > 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
20
+ > `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
18
21
 
19
- ## [G-001] CTO-Frontend 단독 작업 금지 — 흡수된 agency-agents sub-skill 명시 호출 (status: verified)
20
-
21
- - **Why**: walwal-harness v6 는 agency-agents (MIT) 의 design/UX/A11y/perf 패턴을 부서별로 흡수했지만, generator-frontend SKILL 에는 sub-skill 호출 의무가 누락되어 CTO-Frontend 가 단독으로 컴포넌트를 찍어내고 끝나는 회귀가 있었다 (moon_web 2026-05-07).
22
- - **How to apply**:
23
- - **Sprint Workflow Step 2 (구현)** 진입 시, 다음 sub-skill 결과를 **순차 호출 후 본 구현에 반영**한다 — 결과 미반영은 PASS 금지:
24
- 1. **engineering-react-developer** (또는 -flutter-developer) — 컴포넌트 골격 패턴 결정
25
- 2. **design/ux-strategy** (CTO-Designer 흡수) — 사용자 흐름 / 정보 구조 검증
26
- 3. **design/ui-component-spec** — 토큰 / variant / state 매핑
27
- 4. **engineering-accessibility-reviewer** — WCAG AA + 키보드 네비
28
- 5. **engineering-performance-engineer** — RSC 우선 / hydration 비용 / IntersectionObserver
29
- - 호출 결과는 sprint-contract.md FE 섹션의 `## Sub-skill Findings` 블록에 요약 반영. 빈 블록 = FAIL.
30
- - tmux/3D 대시보드에 visibility 보장: 각 sub-skill 호출 직전 `progress.json.meetings.active = ["generator-frontend", "<sub-skill>"]` partial update 후 1.5s, 호출 후 `[]` 로 복귀.
31
- - **References**: skills/cto/SKILL.md "agency-agents (MIT) 흡수" 섹션, skills/generator-designer/SKILL.md.
22
+ <!-- 항목이 추가되면 아래에 기록됩니다 -->
@@ -0,0 +1,22 @@
1
+ ---
2
+ docmeta:
3
+ id: gotchas-meeting-manager
4
+ title: Gotchas — umeeting-umanager
5
+ type: input
6
+ createdAt: 2026-05-08T00:00:00Z
7
+ updatedAt: 2026-05-08T00:00:00Z
8
+ source:
9
+ producer: user
10
+ skillId: harness-dispatcher
11
+ inputs: []
12
+ tags: [gotchas, meeting-manager]
13
+ ---
14
+
15
+ # Gotchas — umeeting-umanager
16
+
17
+ > Dispatcher 가 관리. umeeting-umanager 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
18
+ >
19
+ > 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
20
+ > `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
21
+
22
+ <!-- 항목이 추가되면 아래에 기록됩니다 -->
@@ -1,5 +1,22 @@
1
- # Gotchas — Planner
1
+ ---
2
+ docmeta:
3
+ id: gotchas-planner
4
+ title: Gotchas — uplanner
5
+ type: input
6
+ createdAt: 2026-05-08T00:00:00Z
7
+ updatedAt: 2026-05-08T00:00:00Z
8
+ source:
9
+ producer: user
10
+ skillId: harness-dispatcher
11
+ inputs: []
12
+ tags: [gotchas, planner]
13
+ ---
2
14
 
3
- > Dispatcher가 관리. Planner는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
15
+ # Gotchas — uplanner
16
+
17
+ > Dispatcher 가 관리. uplanner 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
18
+ >
19
+ > 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
20
+ > `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
4
21
 
5
22
  <!-- 항목이 추가되면 아래에 기록됩니다 -->
@@ -1,27 +1,22 @@
1
1
  ---
2
2
  docmeta:
3
- id: service-ops
4
- title: Gotchas — Service-Ops
5
- type: intermediate
6
- createdAt: 2026-05-07T00:00:00Z
7
- updatedAt: 2026-05-07T00:00:00Z
3
+ id: gotchas-service-ops
4
+ title: Gotchas — uservice-uops
5
+ type: input
6
+ createdAt: 2026-05-08T00:00:00Z
7
+ updatedAt: 2026-05-08T00:00:00Z
8
8
  source:
9
- producer: agent
9
+ producer: user
10
10
  skillId: harness-dispatcher
11
11
  inputs: []
12
- tags: [gotcha, service-ops, monitor, presence]
12
+ tags: [gotchas, service-ops]
13
13
  ---
14
14
 
15
- # Gotchas — Service-Ops
15
+ # Gotchas — uservice-uops
16
16
 
17
- > Dispatcher가 관리. Service-Ops 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
17
+ > Dispatcher 가 관리. uservice-uops 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
18
+ >
19
+ > 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
20
+ > `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
18
21
 
19
- ## [G-001] Build/Deploy 진행 중 자리 비움 금지 — monitor 모듈 활성 의무 (status: verified)
20
-
21
- - **Why**: Service-Ops 의 monitor 는 cron + red-alert 이벤트로만 spawn 되도록 설계됐으나, generator/evaluator 가 build·flutter test·deploy 를 실행하는 동안에는 cron 주기 사이 공백이 길어 대시보드에서 "SERVICE-OPS 룸 빈 채" 가 관찰됨 (moon_web 2026-05-07). 빌드 stderr 의 WebSocketChannelException, 컴파일 에러, dart analyze warning 을 실시간으로 캐치하지 못하면 이후 evaluator-functional 단계에서야 발견 → 비용 증가.
22
- - **How to apply**:
23
- - Conductor 가 generator-* 또는 evaluator-functional 을 spawn 하기 **직전**, 동일 tick 에 service-ops/monitor 를 **stream-mode 로 함께 spawn** (handoff-bridge). progress.json.service_ops.monitor.stream_active = true.
24
- - Stream-mode monitor 의 책임: 자식 프로세스 stdout/stderr 를 tail → 정규식 (`error:|exception|TestFailure|Cannot find|Failed to compile`) 매칭 시 즉시 conductor 에 red-alert 발행.
25
- - 자식 프로세스 종료 시 stream_active = false + ops-report 짧은 요약 append.
26
- - Visibility: monitor 활성 동안 progress.json.agents 에 service-ops minifig 가 service-ops 룸 (또는 모니터링 대상 룸 인접) 에 위치하도록 partial update.
27
- - **References**: skills/service-ops/SKILL.md service_ops.monitor 섹션.
22
+ <!-- 항목이 추가되면 아래에 기록됩니다 -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "6.1.2",
3
+ "version": "6.1.4",
4
4
  "description": "Production harness for AI agent engineering — NEXUS-adapted company metaphor (Dispatcher/CEO + Conductor + Meeting-Manager + COO/Planner + CTO + CQO + Service-Ops). Solo/Team mode, Brainstormer, Planner, Generator(BE/FE/Designer/DevOps), Evaluator chain (Code-Quality → Functional → Visual + Architecture/Security). Supports React, Next.js, and Flutter FE stacks.",
5
5
  "bin": {
6
6
  "walwal-harness": "bin/init.js"
@@ -53,6 +53,7 @@
53
53
  "commands/",
54
54
  "assets/",
55
55
  "gotchas/",
56
+ "conventions/",
56
57
  "CHANGELOG.md"
57
58
  ]
58
59
  }
@@ -23,6 +23,10 @@ current_agent=$(jq -r '.current_agent // "null"' "$PROGRESS")
23
23
  agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
24
24
  pipeline=$(jq -r '.pipeline // "null"' "$PROGRESS")
25
25
  mode=$(jq -r '.mode // "auto"' "$PROGRESS")
26
+ # v6.2 — parallel tracks (fork-join) state
27
+ parallel_tracks=$(jq -c '.conductor.tracks // []' "$PROGRESS")
28
+ parallel_rendezvous=$(jq -c '.conductor.rendezvous // null' "$PROGRESS")
29
+ parallel_pending_count=$(jq -r '[ .conductor.tracks // [] | .[] | select(.status == "pending" or .status == "running") ] | length' "$PROGRESS")
26
30
  user_override=$(jq -r '.mode_decision.user_override // "null"' "$PROGRESS")
27
31
  requested_mode=$(jq -r '.service_ops.requested_mode // "null"' "$PROGRESS")
28
32
  planner_requested_mode=$(jq -r '.planner.requested_mode // "null"' "$PROGRESS")
@@ -191,6 +195,80 @@ elif [ "$meetings_active_count" -gt 0 ] && [ "$current_agent" != "meeting-manage
191
195
  next="meeting-manager"
192
196
  action="spawn:meeting-manager:${meeting_type}"
193
197
  new_conductor_state="waiting_meeting"
198
+ if [ "$meeting_type" = "followup-review" ]; then
199
+ new_workflow_stage="followup-review"
200
+ if [ "$current_agent" = "planner" ] && [ "$planner_requested_mode" = "hypothesis-verdict" ]; then
201
+ planner_filter='.planner.requested_mode = null'
202
+ fi
203
+ fi
204
+
205
+ # v6.2 — Parallel tracks advance: if a track owner just completed, mark its track done
206
+ # and dispatch the next pending track. When all tracks completed, convene followup-review.
207
+ #
208
+ # Exclusion: for planner running a hypothesis chain (action_type=hypothesis-*), the track is
209
+ # only terminal when the chain ends with planner.last_brief="hypothesis:done". Intermediate
210
+ # planner ticks (research, verdict-prep, etc.) must defer to the dedicated planner-completed
211
+ # branch which routes the chain.
212
+ elif [ "$parallel_pending_count" -gt 0 ] && [ "$agent_status" = "completed" ] && \
213
+ [ "$(jq --arg a "$current_agent" '[.[] | select(.owner == $a and (.status == "running" or .status == "pending"))] | length' <<<"$parallel_tracks")" -gt 0 ] && \
214
+ ! { [ "$current_agent" = "planner" ] && \
215
+ [ "$(jq --arg a "$current_agent" '[.[] | select(.owner == $a and (.status == "running" or .status == "pending")) | .action_type // ""] | first | startswith("hypothesis")' <<<"$parallel_tracks")" = "true" ] && \
216
+ [ "$planner_last_brief" != "hypothesis:done" ]; }; then
217
+ # mark the matching running/pending track as completed
218
+ updated_tracks=$(jq -c --arg a "$current_agent" '
219
+ [ .[] |
220
+ if .owner == $a and (.status == "running" or .status == "pending") then
221
+ . + { status: "completed", completed_at: (now | todate) }
222
+ else . end
223
+ ]
224
+ ' <<<"$parallel_tracks")
225
+ remaining=$(jq -r '[ .[] | select(.status == "pending" or .status == "running") ] | length' <<<"$updated_tracks")
226
+ if [ "$remaining" -gt 0 ]; then
227
+ # dispatch the next pending track
228
+ next_owner=$(jq -r '[ .[] | select(.status == "pending") ] | .[0].owner // empty' <<<"$updated_tracks")
229
+ next_action=$(jq -r '[ .[] | select(.status == "pending") ] | .[0].action_type // "triage"' <<<"$updated_tracks")
230
+ next_track_id=$(jq -r '[ .[] | select(.status == "pending") ] | .[0].id // "track-?"' <<<"$updated_tracks")
231
+ if [ -n "$next_owner" ]; then
232
+ updated_tracks=$(jq -c --arg id "$next_track_id" '
233
+ [ .[] |
234
+ if .id == $id then . + { status: "running", started_at: (now | todate) } else . end
235
+ ]
236
+ ' <<<"$updated_tracks")
237
+ next="$next_owner"
238
+ action="dispatch:parallel:${next_track_id}:${next_owner}/${next_action}"
239
+ new_workflow_stage="$(meeting_stage_for_owner "$next_owner")"
240
+ if [ "$next_owner" = "planner" ]; then
241
+ planner_filter=".planner.requested_mode = \"$next_action\""
242
+ fi
243
+ meeting_filter=".conductor.tracks = $updated_tracks | .meetings.decision.tracks = $updated_tracks"
244
+ fi
245
+ else
246
+ # All tracks completed → rendezvous (followup-review)
247
+ rdv_type=$(jq -r '.type // "followup-review"' <<<"$parallel_rendezvous")
248
+ fork_meeting_id=$(jq -r '.conductor.fork_meeting_id // ""' "$PROGRESS")
249
+ next="meeting-manager"
250
+ action="convene:${rdv_type}:rendezvous"
251
+ new_conductor_state="waiting_meeting"
252
+ new_workflow_stage="followup-review"
253
+ meeting_prepare=true
254
+ # Save prior_tracks + fork_meeting_id to a stable namespace (.meetings.fork_context)
255
+ # because meeting-doc.sh prepare overwrites .meetings.decision with a new skeleton.
256
+ meeting_filter="
257
+ .meetings.active = [\"meeting-manager\"] |
258
+ .meetings.requested_type = \"$rdv_type\" |
259
+ .meetings.requested_reason = \"rendezvous\" |
260
+ .meetings.requested_tracks = [] |
261
+ .meetings.requested_rendezvous = null |
262
+ .meetings.fork_meeting_id = $(escape_json_string "$fork_meeting_id") |
263
+ .meetings.fork_context = {
264
+ \"fork_meeting_id\": $(escape_json_string "$fork_meeting_id"),
265
+ \"prior_tracks\": $updated_tracks,
266
+ \"sealed_at\": (now | todate)
267
+ } |
268
+ .planner.requested_mode = null |
269
+ .conductor.tracks = $updated_tracks |
270
+ .conductor.rendezvous = null"
271
+ fi
194
272
 
195
273
  elif [ "$requested_mode" != "null" ] && [ -n "$requested_mode" ] && [ "$current_agent" != "service-ops" ]; then
196
274
  next="service-ops"
@@ -224,6 +302,9 @@ elif [ "$current_agent" = "meeting-manager" ] && [ "$agent_status" = "completed"
224
302
  evidence=$(jq -c '.evidence // []' <<<"$meeting_decision")
225
303
  decision_drift=$(jq -r '.drift_classification // "unknown"' <<<"$meeting_decision")
226
304
  source_path=$(jq -r '.source_path // "null"' <<<"$meeting_decision")
305
+ # v6.2 — tracks length >= 2 → fork-join, else single. mode 필드 없음.
306
+ decision_tracks=$(jq -c '.tracks // []' <<<"$meeting_decision")
307
+ decision_rendezvous=$(jq -c '.rendezvous // null' <<<"$meeting_decision")
227
308
 
228
309
  if [ "$owner" = "null" ] || [ -z "$owner" ]; then
229
310
  case "$decision_drift" in
@@ -237,30 +318,95 @@ elif [ "$current_agent" = "meeting-manager" ] && [ "$agent_status" = "completed"
237
318
  esac
238
319
  fi
239
320
 
240
- next="$owner"
241
- action="dispatch:${owner}:${action_type}"
242
- new_workflow_stage="$(meeting_stage_for_owner "$owner")"
243
- if [ "$owner" = "planner" ]; then
244
- planner_filter=".planner.requested_mode = \"$action_type\""
321
+ # v6.2 — Parallel fork: if 2+ tracks, materialize conductor.tracks[] and dispatch
322
+ # the first pending track owner. Subsequent ticks advance remaining tracks.
323
+ if [ "$(jq 'length' <<<"$decision_tracks")" -gt 1 ]; then
324
+ first_track_owner=$(jq -r '.[0].owner' <<<"$decision_tracks")
325
+ first_track_action=$(jq -r '.[0].action_type // "triage"' <<<"$decision_tracks")
326
+ first_track_id=$(jq -r '.[0].id // "track-1"' <<<"$decision_tracks")
327
+ # mark first track as running, rest as pending
328
+ materialized_tracks=$(jq -c --arg first "$first_track_id" '
329
+ [ .[] | . + {
330
+ status: (if .id == $first then "running" else (.status // "pending") end),
331
+ deliverable_path: (.deliverable_path // null)
332
+ }
333
+ ]
334
+ ' <<<"$decision_tracks")
335
+ next="$first_track_owner"
336
+ action="dispatch:parallel:${first_track_id}:${first_track_owner}/${first_track_action}"
337
+ new_workflow_stage="$(meeting_stage_for_owner "$first_track_owner")"
338
+ if [ "$first_track_owner" = "planner" ]; then
339
+ planner_filter=".planner.requested_mode = \"$first_track_action\""
340
+ fi
341
+ meeting_filter="
342
+ .meetings.active = [] |
343
+ .meetings.last_type = .meetings.requested_type |
344
+ .meetings.last_reason = .meetings.requested_reason |
345
+ .meetings.last_decision = \"fork:${first_track_owner}:${first_track_action}\" |
346
+ .meetings.requested_type = null |
347
+ .meetings.requested_reason = null |
348
+ .meetings.requested_tracks = [] |
349
+ .meetings.requested_rendezvous = null |
350
+ .meetings.decision = {
351
+ \"owner\": \"$owner\",
352
+ \"action_type\": \"$action_type\",
353
+ \"rationale\": $(escape_json_string "$rationale"),
354
+ \"evidence\": $evidence,
355
+ \"drift_classification\": $(escape_json_string "$decision_drift"),
356
+ \"source_path\": $(escape_json_string "$source_path"),
357
+ \"tracks\": $materialized_tracks,
358
+ \"rendezvous\": $decision_rendezvous
359
+ } |
360
+ .conductor.tracks = $materialized_tracks |
361
+ .conductor.rendezvous = $decision_rendezvous |
362
+ .conductor.fork_meeting_id = (.meetings.current_id // null)"
363
+ else
364
+ next="$owner"
365
+ action="dispatch:${owner}:${action_type}"
366
+ new_workflow_stage="$(meeting_stage_for_owner "$owner")"
367
+ if [ "$owner" = "planner" ]; then
368
+ planner_filter=".planner.requested_mode = \"$action_type\""
369
+ fi
370
+ meeting_filter="
371
+ .meetings.active = [] |
372
+ .meetings.last_type = .meetings.requested_type |
373
+ .meetings.last_reason = .meetings.requested_reason |
374
+ .meetings.last_decision = \"$owner:$action_type\" |
375
+ .meetings.requested_type = null |
376
+ .meetings.requested_reason = null |
377
+ .meetings.requested_tracks = [] |
378
+ .meetings.requested_rendezvous = null |
379
+ .meetings.decision = {
380
+ \"owner\": \"$owner\",
381
+ \"action_type\": \"$action_type\",
382
+ \"rationale\": $(escape_json_string "$rationale"),
383
+ \"evidence\": $evidence,
384
+ \"drift_classification\": $(escape_json_string "$decision_drift"),
385
+ \"source_path\": $(escape_json_string "$source_path"),
386
+ \"tracks\": [],
387
+ \"rendezvous\": null
388
+ } |
389
+ .conductor.tracks = [] |
390
+ .conductor.rendezvous = null |
391
+ .conductor.fork_meeting_id = null"
245
392
  fi
246
- meeting_filter="
247
- .meetings.active = [] |
248
- .meetings.last_type = .meetings.requested_type |
249
- .meetings.last_reason = .meetings.requested_reason |
250
- .meetings.last_decision = \"$owner:$action_type\" |
251
- .meetings.requested_type = null |
252
- .meetings.requested_reason = null |
253
- .meetings.decision = {
254
- \"owner\": \"$owner\",
255
- \"action_type\": \"$action_type\",
256
- \"rationale\": $(escape_json_string "$rationale"),
257
- \"evidence\": $evidence,
258
- \"drift_classification\": $(escape_json_string "$decision_drift"),
259
- \"source_path\": $(escape_json_string "$source_path")
260
- }"
261
393
 
262
394
  elif [ "$current_agent" = "planner" ] && [ "$agent_status" = "completed" ]; then
263
- if [ "$planner_requested_mode" = "hypothesis" ]; then
395
+ # v6.2 — hypothesis-verdict is terminal for the cell; return to meeting-manager.
396
+ if [ "$planner_requested_mode" = "hypothesis-verdict" ]; then
397
+ next="meeting-manager"
398
+ action="convene:followup-review:hypothesis-verdict"
399
+ new_workflow_stage="followup-review"
400
+ meeting_prepare=true
401
+ planner_filter='.planner.requested_mode = null'
402
+ meeting_filter="
403
+ .meetings.active = [\"meeting-manager\"] |
404
+ .meetings.requested_type = \"followup-review\" |
405
+ .meetings.requested_reason = \"rendezvous\" |
406
+ .meetings.requested_tracks = [] |
407
+ .meetings.requested_rendezvous = null"
408
+ # v6.2 — Accept hypothesis* (e.g. "hypothesis", "hypothesis-validation") as Hypothesis Cell trigger
409
+ elif [[ "$planner_requested_mode" == hypothesis* ]]; then
264
410
  next="documentationer"
265
411
  action="dispatch:hypothesis:documentationer"
266
412
  new_workflow_stage="coo-hypothesis-research"
@@ -21,6 +21,13 @@ ops_report=$(jq -r '.service_ops.auto_retro.last_report // empty' "$PROGRESS")
21
21
  cqo_audit=$(jq -r '.cqo.audit_path // empty' "$PROGRESS")
22
22
  cto_review=$(jq -r '.cto.review_path // empty' "$PROGRESS")
23
23
  current_path=$(jq -r '.meetings.current_record_path // empty' "$PROGRESS")
24
+ # v6.2 — parallel tracks hints (set by conductor before convene). length >= 2 = fork.
25
+ requested_tracks=$(jq -c '.meetings.requested_tracks // []' "$PROGRESS")
26
+ requested_rendezvous=$(jq -c '.meetings.requested_rendezvous // null' "$PROGRESS")
27
+ fork_context=$(jq -c '.meetings.fork_context // null' "$PROGRESS")
28
+ fallback_fork_meeting_id=$(jq -r '.meetings.fork_meeting_id // .conductor.fork_meeting_id // ""' "$PROGRESS")
29
+ fallback_conductor_tracks=$(jq -c '.conductor.tracks // []' "$PROGRESS")
30
+ fallback_decision_tracks=$(jq -c '.meetings.decision.tracks // []' "$PROGRESS")
24
31
 
25
32
  default_decision_json() {
26
33
  local owner="planner"
@@ -35,6 +42,33 @@ default_decision_json() {
35
42
  owner="planner"
36
43
  action_type="goal-alignment"
37
44
  fi
45
+
46
+ # v6.2 — Tracks 가 단일 진실: length >= 2 → fork-join, length == 1 → single.
47
+ # legacy {owner, action_type} 만 들어와도 1-element tracks[] 로 합성.
48
+ local tracks_json="[]"
49
+ local rendezvous_json="null"
50
+ if [ "$(jq 'length' <<<"$requested_tracks")" -gt 1 ]; then
51
+ tracks_json=$(jq -c '
52
+ [ to_entries[] | (.value + {
53
+ id: ((.value.id // null) // ("track-" + ((.key + 1) | tostring))),
54
+ deliverable: (.value.deliverable // "report"),
55
+ deliverable_path: (.value.deliverable_path // null),
56
+ status: (.value.status // "pending")
57
+ })
58
+ ]
59
+ ' <<<"$requested_tracks")
60
+ rendezvous_json=$(jq -c '. // {type:"followup-review", when:"next_cadence"}' <<<"$requested_rendezvous")
61
+ # primary owner = first track owner (편의 미러)
62
+ owner=$(jq -r '.[0].owner // "planner"' <<<"$tracks_json")
63
+ action_type=$(jq -r '.[0].action_type // "triage"' <<<"$tracks_json")
64
+ else
65
+ # single — derive a 1-element tracks[] (backward compat 도 동시에 처리)
66
+ tracks_json=$(jq -c -n \
67
+ --arg owner "$owner" \
68
+ --arg action_type "$action_type" \
69
+ '[ {id:"track-1", owner:$owner, action_type:$action_type, deliverable:"report", deliverable_path:null, status:"pending"} ]')
70
+ fi
71
+
38
72
  jq -n \
39
73
  --arg owner "$owner" \
40
74
  --arg action_type "$action_type" \
@@ -42,7 +76,9 @@ default_decision_json() {
42
76
  --arg drift_classification "$1" \
43
77
  --arg ops_report "$ops_report" \
44
78
  --arg cqo_audit "$cqo_audit" \
45
- --arg cto_review "$cto_review" '
79
+ --arg cto_review "$cto_review" \
80
+ --argjson tracks "$tracks_json" \
81
+ --argjson rendezvous "$rendezvous_json" '
46
82
  {
47
83
  owner: $owner,
48
84
  action_type: $action_type,
@@ -54,26 +90,70 @@ default_decision_json() {
54
90
  (if $cto_review != "" then {source:$cto_review, kind:"cto-review"} else empty end)
55
91
  ]
56
92
  ),
57
- drift_classification: $drift_classification
93
+ drift_classification: $drift_classification,
94
+ tracks: $tracks,
95
+ rendezvous: $rendezvous
58
96
  }'
59
97
  }
60
98
 
99
+ resolve_followup_fork_context() {
100
+ local resolved="$fork_context"
101
+ if [ "$requested_type" != "followup-review" ]; then
102
+ echo "$resolved"
103
+ return
104
+ fi
105
+
106
+ if [ "$resolved" != "null" ] && [ -n "$resolved" ]; then
107
+ echo "$resolved"
108
+ return
109
+ fi
110
+
111
+ local fallback_tracks="$fallback_conductor_tracks"
112
+ if [ "$(jq 'length' <<<"$fallback_tracks")" -eq 0 ] && [ "$(jq 'length' <<<"$fallback_decision_tracks")" -gt 0 ]; then
113
+ fallback_tracks="$fallback_decision_tracks"
114
+ fi
115
+
116
+ if [ "$(jq 'length' <<<"$fallback_tracks")" -eq 0 ]; then
117
+ echo "null"
118
+ return
119
+ fi
120
+
121
+ jq -c -n \
122
+ --arg fork_meeting_id "$fallback_fork_meeting_id" \
123
+ --argjson prior_tracks "$fallback_tracks" \
124
+ '{fork_meeting_id:$fork_meeting_id, prior_tracks:$prior_tracks, sealed_at:(now | todate)}'
125
+ }
126
+
61
127
  if [ "$MODE" = "prepare" ]; then
62
128
  meeting_id="M-$(date -u +%Y%m%dT%H%M%SZ)"
63
129
  meeting_dir="$MEETINGS_ROOT/$meeting_id"
64
130
  mkdir -p "$meeting_dir"
65
131
  record="$meeting_dir/meeting-$meeting_id.md"
66
132
 
133
+ # v6.2 — Notice surfaces tracks/rendezvous for visibility (length >= 2 = fork)
134
+ tracks_summary="(single owner)"
135
+ rendezvous_summary="-"
136
+ is_fork="false"
137
+ if [ "$(jq 'length' <<<"$requested_tracks")" -gt 1 ]; then
138
+ is_fork="true"
139
+ tracks_summary=$(jq -r '[.[] | "\(.id // "?"):\(.owner)/\(.action_type)→\(.deliverable // "report")"] | join(", ")' <<<"$requested_tracks")
140
+ rendezvous_summary=$(jq -r '"\(.type // "followup-review")@\(.when // "next_cadence")"' <<<"$requested_rendezvous")
141
+ fi
67
142
  cat > "$meeting_dir/notice.md" <<EOF
68
143
  # Notice — $meeting_id
69
144
 
70
145
  - type: $requested_type
71
146
  - reason: $requested_reason
72
147
  - goal_adherence: $goal_adherence
148
+ - fork: $is_fork
149
+ - tracks: $tracks_summary
150
+ - rendezvous: $rendezvous_summary
73
151
 
74
152
  ## Request
75
153
  "$requested_reason" 요청이 접수되었습니다. 참석자들은 각자의 전문 시각에서 사실·증거를 제출하고,
76
154
  다음 owner와 action_type을 evidence 중심으로 결정하십시오.
155
+
156
+ > **Parallel tracks (v6.2)**: \`tracks[]\` 의 길이 ≥ 2 면 fork-join. \`rendezvous\` 시점에 followup-review 로 합쳐집니다. 단일 트랙 회의는 기존과 동일하게 동작합니다.
77
157
  EOF
78
158
 
79
159
  cat > "$meeting_dir/prep-dispatcher.md" <<'EOF'
@@ -108,6 +188,26 @@ EOF
108
188
  EOF
109
189
 
110
190
  decision_json="$(default_decision_json "$drift")"
191
+
192
+ # v6.2 — followup-review: surface prior_tracks + fork_meeting_id from fork_context
193
+ resolved_fork_context="$(resolve_followup_fork_context)"
194
+
195
+ fork_section=""
196
+ if [ "$requested_type" = "followup-review" ] && [ "$resolved_fork_context" != "null" ] && [ -n "$resolved_fork_context" ]; then
197
+ fork_id=$(jq -r '.fork_meeting_id // ""' <<<"$resolved_fork_context")
198
+ fork_section="
199
+
200
+ ## Fork Context (v6.2)
201
+ - fork_meeting_id: $fork_id
202
+ - prior_tracks (rendezvous 입력):
203
+
204
+ \`\`\`json
205
+ $(jq '.prior_tracks' <<<"$resolved_fork_context")
206
+ \`\`\`
207
+
208
+ > 결정자(기본 CTO) 는 위 트랙별 deliverable 을 통합 검토 후 \`apply-now / backlog / more-validation\` 중 하나를 선택해 아래 Decision JSON 의 \`tracks\` 를 비우고 \`owner / action_type\` 만 채운다 (followup 에서 또 fork 금지)."
209
+ fi
210
+
111
211
  cat > "$record" <<EOF
112
212
  # Meeting Record — $meeting_id
113
213
 
@@ -115,6 +215,7 @@ EOF
115
215
  - type: $requested_type
116
216
  - reason: $requested_reason
117
217
  - goal_adherence: $goal_adherence
218
+ $fork_section
118
219
 
119
220
  ## Decision JSON
120
221
  \`\`\`json
@@ -135,6 +236,10 @@ EOF
135
236
  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
237
  )"))" >/dev/null
137
238
 
239
+ if [ "$requested_type" = "followup-review" ] && [ "$resolved_fork_context" != "null" ] && [ -n "$resolved_fork_context" ]; then
240
+ bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" ".meetings.fork_context = $resolved_fork_context" >/dev/null
241
+ fi
242
+
138
243
  echo "${record#$PROJECT_ROOT/}"
139
244
  exit 0
140
245
  fi
@@ -150,4 +255,43 @@ if [ "$MODE" = "read-decision" ]; then
150
255
  exit 0
151
256
  fi
152
257
 
258
+ # v6.2 — read-tracks: emit normalized tracks[] (length>=2 = fork, length==1 = single).
259
+ # Legacy decisions without tracks → synthesize 1-element tracks[] from {owner, action_type}.
260
+ if [ "$MODE" = "read-tracks" ]; then
261
+ jq -c '
262
+ if ((.meetings.decision.tracks // []) | length) > 0 then
263
+ .meetings.decision.tracks
264
+ elif ((.meetings.fork_context.prior_tracks // []) | length) > 0 then
265
+ .meetings.fork_context.prior_tracks
266
+ elif ((.conductor.tracks // []) | length) > 0 then
267
+ .conductor.tracks
268
+ else
269
+ [ {
270
+ id: "track-1",
271
+ owner: (.meetings.decision.owner // "planner"),
272
+ action_type: (.meetings.decision.action_type // "triage"),
273
+ deliverable: "report",
274
+ deliverable_path: null,
275
+ status: "pending"
276
+ }
277
+ ]
278
+ end
279
+ ' "$PROGRESS"
280
+ exit 0
281
+ fi
282
+
283
+ # v6.2 — read-rendezvous: emit rendezvous{} or null
284
+ if [ "$MODE" = "read-rendezvous" ]; then
285
+ jq -c '.meetings.decision.rendezvous // .conductor.rendezvous // null' "$PROGRESS"
286
+ exit 0
287
+ fi
288
+
289
+ # v6.2 — read-fork: "true" if tracks.length >= 2, else "false"
290
+ if [ "$MODE" = "read-fork" ]; then
291
+ decision="$(bash "$0" "$PROJECT_ROOT" read-decision 2>/dev/null || true)"
292
+ if [ -z "$decision" ]; then decision='{}'; fi
293
+ jq -r '((.tracks // []) | length) > 1' <<<"$decision"
294
+ exit 0
295
+ fi
296
+
153
297
  exit 2
@@ -8,10 +8,12 @@
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  LIB="$SCRIPT_DIR/lib/harness-render-progress.sh"
10
10
  AUDIT_LIB="$SCRIPT_DIR/lib/harness-audit.sh"
11
+ MIGRATE_LIB="$SCRIPT_DIR/lib/harness-progress-migrate.sh"
11
12
 
12
13
  if [ ! -f "$LIB" ]; then exit 0; fi
13
14
  source "$LIB"
14
15
  [ -f "$AUDIT_LIB" ] && source "$AUDIT_LIB"
16
+ [ -f "$MIGRATE_LIB" ] && source "$MIGRATE_LIB"
15
17
  command -v jq &>/dev/null || exit 0
16
18
 
17
19
  PROJECT_ROOT="$(resolve_harness_root "." 2>/dev/null)" || exit 0
@@ -20,6 +22,11 @@ CONFIG="$PROJECT_ROOT/.harness/config.json"
20
22
  HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
21
23
  [ -f "$PROGRESS" ] || exit 0
22
24
 
25
+ # v6.2 — idempotent schema migration (parallel tracks fields, etc.)
26
+ if declare -f migrate_progress_schema >/dev/null 2>&1; then
27
+ migrate_progress_schema "$PROGRESS" || true
28
+ fi
29
+
23
30
  sprint_status=$(jq -r '.sprint.status // "init"' "$PROGRESS" 2>/dev/null)
24
31
  sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS" 2>/dev/null)
25
32
  current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS" 2>/dev/null)