@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.
- package/CHANGELOG.md +31 -0
- package/README.md +4 -2
- package/assets/templates/HARNESS.md +216 -403
- package/assets/templates/config.json +3 -3
- package/conventions/README.md +92 -0
- package/conventions/conductor.md +24 -0
- package/conventions/coo-developer.md +24 -0
- package/conventions/cqo.md +24 -0
- package/conventions/cto.md +24 -0
- package/conventions/dispatcher.md +24 -0
- package/conventions/documentationer.md +24 -0
- package/conventions/evaluator-architecture.md +24 -0
- package/conventions/evaluator-code-quality.md +24 -0
- package/conventions/evaluator-functional.md +24 -0
- package/conventions/evaluator-security.md +24 -0
- package/conventions/evaluator-visual.md +24 -0
- package/conventions/generator-backend.md +24 -0
- package/conventions/generator-designer.md +24 -0
- package/conventions/generator-devops.md +24 -0
- package/conventions/generator-frontend.md +24 -0
- package/conventions/meeting-manager.md +24 -0
- package/conventions/planner.md +24 -0
- package/conventions/service-ops.md +24 -0
- package/conventions/shared.md +24 -0
- package/gotchas/README.md +40 -8
- package/gotchas/conductor.md +12 -82
- package/gotchas/coo-developer.md +21 -12
- package/gotchas/cqo.md +22 -0
- package/gotchas/cto.md +22 -0
- package/gotchas/dispatcher.md +12 -87
- package/gotchas/documentationer.md +21 -12
- package/gotchas/evaluator-architecture.md +22 -0
- package/gotchas/evaluator-code-quality.md +11 -20
- package/gotchas/evaluator-functional.md +10 -30
- package/gotchas/evaluator-security.md +22 -0
- package/gotchas/evaluator-visual.md +10 -19
- package/gotchas/generator-backend-laravel.md +11 -77
- package/gotchas/generator-backend.md +19 -2
- package/gotchas/generator-designer.md +22 -0
- package/gotchas/generator-devops.md +22 -0
- package/gotchas/generator-frontend.md +13 -22
- package/gotchas/meeting-manager.md +22 -0
- package/gotchas/planner.md +19 -2
- package/gotchas/service-ops.md +13 -18
- package/package.json +2 -1
- package/scripts/conductor-tick.sh +167 -21
- package/scripts/harness-meeting-doc.sh +146 -2
- package/scripts/harness-session-start.sh +7 -0
- package/scripts/lib/harness-progress-migrate.sh +58 -0
- package/skills/conductor/SKILL.md +1 -0
- package/skills/coo-developer/SKILL.md +35 -5
- package/skills/documentationer/SKILL.md +55 -9
- package/skills/meeting-manager/SKILL.md +114 -4
- 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 —
|
|
5
|
-
type:
|
|
6
|
-
createdAt: 2026-05-
|
|
7
|
-
updatedAt: 2026-05-
|
|
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:
|
|
9
|
+
producer: user
|
|
10
10
|
skillId: harness-dispatcher
|
|
11
11
|
inputs: []
|
|
12
|
-
tags: [
|
|
12
|
+
tags: [gotchas, generator-frontend]
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
# Gotchas —
|
|
15
|
+
# Gotchas — ugenerator-ufrontend
|
|
16
16
|
|
|
17
|
-
> Dispatcher가 관리.
|
|
17
|
+
> Dispatcher 가 관리. ugenerator-ufrontend 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
|
|
18
|
+
>
|
|
19
|
+
> 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
|
|
20
|
+
> `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
|
|
18
21
|
|
|
19
|
-
|
|
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
|
+
<!-- 항목이 추가되면 아래에 기록됩니다 -->
|
package/gotchas/planner.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
15
|
+
# Gotchas — uplanner
|
|
16
|
+
|
|
17
|
+
> Dispatcher 가 관리. uplanner 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
|
|
18
|
+
>
|
|
19
|
+
> 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
|
|
20
|
+
> `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
|
|
4
21
|
|
|
5
22
|
<!-- 항목이 추가되면 아래에 기록됩니다 -->
|
package/gotchas/service-ops.md
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
docmeta:
|
|
3
|
-
id: service-ops
|
|
4
|
-
title: Gotchas —
|
|
5
|
-
type:
|
|
6
|
-
createdAt: 2026-05-
|
|
7
|
-
updatedAt: 2026-05-
|
|
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:
|
|
9
|
+
producer: user
|
|
10
10
|
skillId: harness-dispatcher
|
|
11
11
|
inputs: []
|
|
12
|
-
tags: [
|
|
12
|
+
tags: [gotchas, service-ops]
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
# Gotchas —
|
|
15
|
+
# Gotchas — uservice-uops
|
|
16
16
|
|
|
17
|
-
> Dispatcher가 관리.
|
|
17
|
+
> Dispatcher 가 관리. uservice-uops 는 세션 시작 시 이 파일을 읽고 같은 실수를 반복하지 않습니다.
|
|
18
|
+
>
|
|
19
|
+
> 항목 형식은 `gotchas/README.md` 참조. Dispatcher 가 사용자의 실수 지적을 감지하면 자동으로
|
|
20
|
+
> `### [G-NNN]` 항목을 append 합니다. 사용자가 직접 편집해도 무방합니다.
|
|
18
21
|
|
|
19
|
-
|
|
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.
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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)
|