@walwal-harness/cli 6.1.1 → 6.1.3

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 (36) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +306 -367
  3. package/assets/templates/config.json +7 -0
  4. package/assets/templates/progress.json.template +13 -0
  5. package/gotchas/README.md +40 -8
  6. package/gotchas/conductor.md +12 -82
  7. package/gotchas/coo-developer.md +21 -12
  8. package/gotchas/cqo.md +22 -0
  9. package/gotchas/cto.md +22 -0
  10. package/gotchas/dispatcher.md +12 -87
  11. package/gotchas/documentationer.md +21 -12
  12. package/gotchas/evaluator-architecture.md +22 -0
  13. package/gotchas/evaluator-code-quality.md +11 -20
  14. package/gotchas/evaluator-functional.md +10 -30
  15. package/gotchas/evaluator-security.md +22 -0
  16. package/gotchas/evaluator-visual.md +10 -19
  17. package/gotchas/generator-backend-laravel.md +11 -77
  18. package/gotchas/generator-backend.md +19 -2
  19. package/gotchas/generator-designer.md +22 -0
  20. package/gotchas/generator-devops.md +22 -0
  21. package/gotchas/generator-frontend.md +13 -22
  22. package/gotchas/meeting-manager.md +22 -0
  23. package/gotchas/planner.md +19 -2
  24. package/gotchas/service-ops.md +13 -18
  25. package/package.json +1 -1
  26. package/scripts/conductor-tick.sh +167 -21
  27. package/scripts/harness-meeting-doc.sh +146 -2
  28. package/scripts/harness-session-start.sh +61 -0
  29. package/scripts/harness-token-limit.sh +99 -0
  30. package/scripts/harness-user-prompt-submit.sh +23 -0
  31. package/scripts/lib/harness-progress-migrate.sh +58 -0
  32. package/skills/conductor/SKILL.md +1 -0
  33. package/skills/coo-developer/SKILL.md +35 -5
  34. package/skills/documentationer/SKILL.md +55 -9
  35. package/skills/meeting-manager/SKILL.md +114 -4
  36. package/skills/planner/SKILL.md +45 -0
@@ -1,85 +1,19 @@
1
1
  ---
2
2
  docmeta:
3
3
  id: gotchas-generator-backend-laravel
4
- title: Generator-Backend Gotchas — Laravel/Livewire/Filament
5
- type: output
6
- createdAt: 2026-05-07T00:00:00Z
7
- updatedAt: 2026-05-07T00:00:00Z
4
+ title: Gotchas — Generator-Backend (Laravel/Livewire/Filament)
5
+ type: input
6
+ createdAt: 2026-05-08T00:00:00Z
7
+ updatedAt: 2026-05-08T00:00:00Z
8
8
  source:
9
- producer: agent
10
- skillId: harness-planner
11
- inputs:
12
- - documentId: agency-agents-engineering
13
- uri: https://github.com/msitarzewski/agency-agents
14
- relation: output-from
15
- sections:
16
- - sourceRange: { startLine: 1, endLine: 1 }
17
- targetRange: { startLine: 1, endLine: 999 }
18
- tags: [gotcha, generator-backend, laravel, livewire, filament, phase-b]
9
+ producer: user
10
+ skillId: harness-dispatcher
11
+ inputs: []
12
+ tags: [gotchas, generator-backend, laravel]
19
13
  ---
20
14
 
21
- <!-- Source: https://github.com/msitarzewski/agency-agents (MIT) -->
15
+ # Gotchas — Generator-Backend · Laravel/Livewire/Filament
22
16
 
23
- # Generator-Backend Gotchas — Laravel/Livewire/Filament
17
+ > 스택별 가드. Generator-Backend 는 Laravel 스택을 감지하면 `gotchas/generator-backend.md` 와 함께 이 파일을 추가로 읽습니다.
24
18
 
25
- > 스택별 가드. 공통 가드는 `gotchas/generator-backend.md` 참조.
26
- > 모든 항목은 `unverified` 상태로 시작, Planner 리뷰 후 `verified` 승격.
27
-
28
- ## verified
29
-
30
- (없음 — 첫 리비전)
31
-
32
- ## unverified
33
-
34
- ### G-LV-001 — Livewire 컴포넌트가 Eloquent 모델을 prop으로 받을 때 직렬화 충돌
35
- - **언제**: 부모 컴포넌트 → 자식 컴포넌트로 모델을 직접 props 전달
36
- - **증상**: 새 요청마다 모델 hydrate 실패, lazy load 시 fresh 로드되어 데이터 불일치
37
- - **회피**: ID만 전달 + 자식이 자기 책임으로 fetch, 또는 `serializeable=true` 명시 + 변경 위험 인지
38
-
39
- ### G-LV-002 — `wire:model.live` 무차별 사용으로 N+1 트래픽 폭발
40
- - **증상**: 입력 1자마다 서버 왕복, 운영에서 큐 폭주
41
- - **회피**: 폼 입력 default `wire:model.live.debounce.500ms` 또는 `wire:model.lazy`
42
-
43
- ### G-FL-001 — Filament Resource `eagerLoad()` 누락
44
- - **증상**: 인덱스 페이지 1회 로드에 컬럼 수 × 행 수 만큼 쿼리 (N+1)
45
- - **회피**: Resource에 `protected $eagerLoadRelations = ['user', 'category']` 명시. Eval-Architecture가 검출.
46
-
47
- ### G-FL-002 — Filament Tenancy panel에서 Global scope 누락
48
- - **증상**: 한 테넌트 사용자가 다른 테넌트 데이터 노출
49
- - **회피**: `getEloquentQuery()` 오버라이드 시 `parent::getEloquentQuery()` 후 scope 추가. Eval-Security 적발.
50
-
51
- ### G-EL-001 — `whereHas` 다단 사용으로 인덱스 미사용 쿼리
52
- - **증상**: relation 깊이 ≥ 2 + 결과 set 크면 secs 단위
53
- - **회피**: subquery 또는 `whereIn(select 1 column)` 패턴, EXPLAIN 첨부
54
-
55
- ### G-EL-002 — Mass assignment 누락 → 권한 우회
56
- - **증상**: 사용자가 `is_admin` 같은 컬럼을 요청 본문에 끼워 권한 상승
57
- - **회피**: 모든 모델 `$fillable` 또는 `$guarded = []` 의도 명시. Eval-Security CRITICAL.
58
-
59
- ### G-MIG-001 — 인덱스 없는 외래키 컬럼
60
- - **증상**: parent delete 시 child scan 풀스캔
61
- - **회피**: 마이그레이션에서 `foreignId(...)->index()` 명시
62
-
63
- ### G-JOB-001 — Job 안에서 retry 비대응 외부 호출
64
- - **증상**: Stripe·외부 API 호출이 retry 시 중복 결제·중복 메시지
65
- - **회피**: idempotency_key 또는 `ShouldBeUnique`. Eval-Security 검토.
66
-
67
- ### G-CACHE-001 — `Cache::tags()` on file driver
68
- - **증상**: 운영에서 silent fail (tag 무시)
69
- - **회피**: prod cache driver는 redis 강제. config/cache.php boot 시 검증.
70
-
71
- ### G-AUTH-001 — Sanctum 토큰 회전 정책 부재
72
- - **증상**: 토큰 영구 유효 → 누출 시 영향 무한대
73
- - **회피**: TTL + 사용 흐름별 ability 분리, 회전 명령 정의 (DevOps와 연계)
74
-
75
- ### G-OBS-001 — observer chain → 부작용 폭발
76
- - **증상**: User saving → Profile observer → Account observer → 무한 루프 또는 dead-lock
77
- - **회피**: observer 안에서 다른 모델 save 금지 룰. 명시적 Action 호출로 대체.
78
-
79
- ### G-FORM-001 — `$request->validate()` 인라인 사용
80
- - **증상**: 검증 룰 재사용·테스트·문서화 모두 어려움
81
- - **회피**: FormRequest 클래스 강제. Eval-CodeQuality lint 룰.
82
-
83
- ### G-PERF-001 — Telescope prod 활성
84
- - **증상**: 모든 쿼리·요청 적재 → DB 비대 + 보안 노출
85
- - **회피**: env 분리, prod에서 `TelescopeServiceProvider` 등록 차단. Eval-Security 적발.
19
+ <!-- 항목이 추가되면 아래에 기록됩니다 -->
@@ -1,5 +1,22 @@
1
- # Gotchas — Generator-Backend
1
+ ---
2
+ docmeta:
3
+ id: gotchas-generator-backend
4
+ title: Gotchas — ugenerator-ubackend
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-backend]
13
+ ---
2
14
 
3
- > Dispatcher가 관리. Generator-Backend는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
15
+ # Gotchas — ugenerator-ubackend
16
+
17
+ > Dispatcher 가 관리. ugenerator-ubackend 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
18
+ >
19
+ > 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
20
+ > `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
4
21
 
5
22
  <!-- 항목이 추가되면 아래에 기록됩니다 -->
@@ -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.1",
3
+ "version": "6.1.3",
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"
@@ -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"