@walwal-harness/cli 6.0.5 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/commands/harness-solo.md +20 -20
  3. package/commands/harness-team.md +1 -1
  4. package/package.json +1 -1
  5. package/scripts/conductor-tick.sh +382 -0
  6. package/scripts/harness-archive.sh +123 -0
  7. package/scripts/harness-dashboard-up.sh +72 -0
  8. package/scripts/harness-dashboard.sh +509 -0
  9. package/scripts/harness-goal-init.sh +72 -0
  10. package/scripts/harness-goal-show.sh +37 -0
  11. package/scripts/harness-gotcha-memory.sh +348 -0
  12. package/scripts/harness-gotcha-register.sh +393 -0
  13. package/scripts/harness-meeting-doc.sh +153 -0
  14. package/scripts/harness-monitor.sh +398 -0
  15. package/scripts/harness-next.sh +577 -0
  16. package/scripts/harness-progress-set.sh +41 -0
  17. package/scripts/harness-prompt-history.sh +164 -0
  18. package/scripts/harness-queue-manager.sh +694 -0
  19. package/scripts/harness-session-start.sh +140 -0
  20. package/scripts/harness-statusline.sh +139 -0
  21. package/scripts/harness-task-session.sh +48 -0
  22. package/scripts/harness-tmux.sh +372 -0
  23. package/scripts/harness-user-prompt-submit.sh +123 -0
  24. package/scripts/init-agents-md.sh +337 -0
  25. package/scripts/init-ref-docs.sh +262 -0
  26. package/scripts/lib/harness-audit.sh +113 -0
  27. package/scripts/lib/harness-guardrail.sh +148 -0
  28. package/scripts/lib/harness-keywait.sh +57 -0
  29. package/scripts/lib/harness-render-progress.sh +346 -0
  30. package/scripts/scan-project.sh +367 -0
  31. package/skills/brainstorming/SKILL.md +4 -4
  32. package/skills/conductor/SKILL.md +12 -16
  33. package/skills/cqo/SKILL.md +7 -0
  34. package/skills/cto/SKILL.md +7 -0
  35. package/skills/dispatcher/SKILL.md +17 -28
  36. package/skills/evaluator-code-quality/SKILL.md +1 -1
  37. package/skills/evaluator-functional/SKILL.md +1 -1
  38. package/skills/evaluator-visual/SKILL.md +1 -1
  39. package/skills/generator-backend/SKILL.md +1 -1
  40. package/skills/generator-frontend/SKILL.md +1 -1
  41. package/skills/meeting-manager/SKILL.md +33 -1
  42. package/skills/planner/SKILL.md +23 -9
  43. package/skills/service-ops/SKILL.md +19 -4
package/CHANGELOG.md CHANGED
@@ -29,6 +29,48 @@ docmeta:
29
29
 
30
30
  # Changelog
31
31
 
32
+ ## 6.1.0 — document-driven company loop + task-session isolation (2026-05-08)
33
+
34
+ ### Why
35
+ v6.0.x 는 회사형 조직 비유와 Cxx 라우팅을 문서로는 설명했지만, 실제 런타임은 여전히 얕았다. 특히 다음 두 결함이 컸다.
36
+
37
+ - 회의 결과가 `누가 다음 owner 인가` 를 구조적으로 기록하지 않음
38
+ - `goal_adherence` 하락 시 원인 분류 없이 Planner로 보내는 경향이 있음
39
+
40
+ 또한 agent 간 자기강화 편향을 줄이기 위한 문서 중심 task session 분리도 충분히 강제되지 않았다.
41
+
42
+ ### Changes
43
+ - **document-driven meeting decision**
44
+ - `meeting-manager` 가 `notice.md`, `prep-*.md`, `meeting-<id>.md` skeleton 을 자동 생성
45
+ - 회의 기록에 `decision.owner`, `action_type`, `rationale`, `evidence`, `drift_classification` JSON 블록 추가
46
+ - `conductor-tick` 은 더 이상 `meeting_reason` 문자열만 보지 않고 회의 decision 문서를 우선 읽어 다음 owner 를 결정
47
+ - **goal drift classification**
48
+ - `implementation_drift | planning_drift | ops_drift | goal_drift` 분류 추가
49
+ - 분류별 기본 라우팅:
50
+ - `implementation_drift -> cto`
51
+ - `planning_drift -> planner`
52
+ - `ops_drift -> service-ops`
53
+ - `goal_drift -> dispatcher`
54
+ - **task-session isolation**
55
+ - `harness-next` 가 다음 agent 마다 `.harness/actions/task-sessions/<agent>/<id>.md` 생성
56
+ - `handoff.json` 에 `task_session_path` 추가
57
+ - `UserPromptSubmit` 가 running agent 와 다른 `/harness-*` 호출을 경고가 아니라 **hard block**
58
+ - **direct invocation policy**
59
+ - 모든 harness skill 의 `disable-model-invocation` 을 `false` 로 통일
60
+ - orchestration/control-plane 과 worker/evaluator 모두 직접 호출 가능 상태로 정리
61
+ - **archive reset hardening**
62
+ - archive 시 `workflow`, `meetings.decision`, `service_ops.drift_classification`, `task_sessions.current` 초기화
63
+
64
+ ### Validation
65
+ - `bash -n` 으로 주요 스크립트 문법 검증
66
+ - `jq empty` 로 `config.json`, `progress.json` 검증
67
+ - `/private/tmp` 샌드박스에서:
68
+ - `dispatcher -> meeting-manager` 시 회의 문서 자동 생성 확인
69
+ - 회의 decision 편집 후 `dispatcher / planner / cto` 로 문서 기반 재라우팅 확인
70
+ - `goal_adherence` 하락 시 drift classification 반영 확인
71
+ - context isolation hard block 확인
72
+ - `task_session_path` 생성 확인
73
+
32
74
  ## 6.0.5 — migrate 가 gotcha entry / bundle version 까지 sync (2026-05-07)
33
75
 
34
76
  ### Why
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  docmeta:
3
3
  id: harness-solo
4
- title: /harness-solo — Solo Mode 시작/전환
4
+ title: /harness-solo — 비상용 Single-Agent Fallback
5
5
  type: input
6
6
  createdAt: 2026-04-20T00:00:00Z
7
7
  updatedAt: 2026-05-07T00:00:00Z
@@ -24,14 +24,14 @@ docmeta:
24
24
  tags: [harness, solo-mode, command, override, v6]
25
25
  ---
26
26
 
27
- # /harness-solo — Solo Mode 사용자 Override (v6.0+)
27
+ # /harness-solo — 비상용 Single-Agent Fallback (v6.0+)
28
28
 
29
- > v6.0 부터 모드 결정은 **Conductor 가 자동**으로 합니다 (config.json `mode_selection.rules`). 본 명령은 Conductor 의 자동 결정을 사용자가 강제로 덮어쓰는 **override** 입니다.
29
+ > v6.0 의 정상 경로는 회사형 자동 루프입니다. 본 명령은 그 루프를 일시적으로 벗어나는 **비상용 override** 입니다.
30
30
  >
31
31
  > **효과**: `progress.json.mode = "solo"` 강제 + `mode_decision.user_override = "solo"` 기록. 현재 sprint 끝까지 유지. 다음 sprint 진입 시 Conductor 가 재자동결정. **Auto 복귀**: 사용자 발화 "auto 로 돌려" 또는 "Conductor 결정으로".
32
32
 
33
- 프롬프트 기반으로 하네스 파이프라인을 순차 진행합니다.
34
- Team 모드에서 전환 시, 진행 중이던 피처의 상태를 보존하고 Solo로 이어갑니다.
33
+ 디버깅, 스크립트 장애, 수동 hotfix 같은 예외 상황에서만 사용합니다.
34
+ 회사 루프에서 전환 시, 진행 중이던 피처의 상태를 보존하고 단일 에이전트 fallback 으로 이어갑니다.
35
35
 
36
36
  ## 실행 절차
37
37
 
@@ -49,12 +49,12 @@ echo "현재 모드: $MODE"
49
49
  bash scripts/harness-queue-manager.sh recover .
50
50
 
51
51
  # mode를 solo로 설정
52
- jq '.mode = "solo" | .team_state.active_teams = 0 | .team_state.paused_at = (now | todate)' .harness/progress.json > /tmp/progress_tmp.json && mv /tmp/progress_tmp.json .harness/progress.json
52
+ jq '.mode = "solo" | .mode_decision.user_override = "solo" | .team_state.active_teams = 0 | .team_state.paused_at = (now | todate)' .harness/progress.json > /tmp/progress_tmp.json && mv /tmp/progress_tmp.json .harness/progress.json
53
53
  ```
54
54
 
55
55
  **paused → solo 전환 시:**
56
56
  ```bash
57
- jq '.mode = "solo"' .harness/progress.json > /tmp/progress_tmp.json && mv /tmp/progress_tmp.json .harness/progress.json
57
+ jq '.mode = "solo" | .mode_decision.user_override = "solo"' .harness/progress.json > /tmp/progress_tmp.json && mv /tmp/progress_tmp.json .harness/progress.json
58
58
  ```
59
59
 
60
60
  ### Step 2: 현재 진행 상태 표시
@@ -75,29 +75,29 @@ fi
75
75
  progress.json의 `next_agent` 필드를 확인하여 다음 단계를 안내합니다:
76
76
 
77
77
  - **next_agent = "dispatcher"** → "프롬프트로 요구사항을 입력하면 자동으로 dispatcher가 분석합니다."
78
- - **next_agent = "planner"** → "/harness-planner 를 호출하거나, 계획을 요청하세요."
79
- - **next_agent = "generator-backend"** → "/harness-generator-backend 를 호출하세요."
80
- - **next_agent = "generator-frontend"** → "/harness-generator-frontend 를 호출하세요."
81
- - **next_agent = "evaluator-functional"** → "/harness-evaluator-functional 를 호출하세요."
82
- - **next_agent = "evaluator-visual"** → "/harness-evaluator-visual 를 호출하세요."
78
+ - **next_agent = "planner"** → Planner 단계가 다음입니다. SessionStart / 내부 handoff가 연결돼 있으면 자동 진입, 아니면 해당 스킬을 수동 호출합니다.
79
+ - **next_agent = "generator-backend"** → Backend generator 단계가 다음입니다.
80
+ - **next_agent = "generator-frontend"** → Frontend generator 단계가 다음입니다.
81
+ - **next_agent = "evaluator-functional"** → Functional evaluator 단계가 다음입니다.
82
+ - **next_agent = "evaluator-visual"** → Visual evaluator 단계가 다음입니다.
83
83
 
84
84
  ### Step 4: Solo 모드 진행
85
85
 
86
- 각 에이전트는 완료 즉시 **`/harness-next` 슬래시 명령을 자동 호출** 하여 다음 에이전트로 핸드오프합니다 (사용자가 매번 bash 명령을 칠 필요 없음).
86
+ 각 에이전트는 완료 즉시 **내부 handoff(`harness-next.sh` 또는 SessionStart hook)** 로 다음 에이전트에 넘겨집니다. Owner가 `/harness-next` 를 직접 다룬다는 전제는 6.0 회사 모델과 맞지 않습니다.
87
87
 
88
88
  **예외 — 사용자 승인 게이트가 있는 단계:**
89
- - **Brainstorming**: brainstorm-spec.md 완성 후 사용자 승인 대기 → 승인 시 자동 `/harness-next`
90
- - **Planner**: plan.md / api-contract.json 완성 후 사용자 승인 대기 → 승인 시 자동 `/harness-next`
89
+ - **Brainstorming**: brainstorm-spec.md 완성 후 사용자 승인 대기 → 승인 시 내부 handoff
90
+ - **Planner**: plan.md / api-contract.json 완성 후 사용자 승인 대기 → 승인 시 내부 handoff
91
91
 
92
- 흐름이 멈췄거나 수동으로 다음 단계를 트리거하고 싶으면 사용자가 직접 `/harness-next` 입력 가능.
92
+ 흐름이 멈췄다면 원인은 보통 내부 handoff 스크립트/훅 미설치 또는 `next_agent` 불일치입니다. 이 경우 상태 파일과 hook 설치 상태를 먼저 점검합니다.
93
93
 
94
94
  feature-queue.json이 존재하는 경우:
95
95
  - 피처 완료(evaluator PASS) 시 `bash scripts/harness-queue-manager.sh pass {FEATURE_ID} .`를 호출하여 공유 상태 업데이트
96
96
  - Team 모드로 전환해도 이미 완료된 피처는 skip됨
97
97
 
98
- ## Solo 모드에서 가능한 작업
98
+ ## 사용 권장 상황
99
99
 
100
100
  - 프롬프트로 각 에이전트 순차 실행
101
- - 코드 직접 수정 및 디버깅
102
- - 피처 단위 작업 선택
103
- - Team 모드로 언제든 전환: `/harness-team`
101
+ - 내부 handoff 스크립트 장애 시 응급 복구
102
+ - 아주 짧은 수동 hotfix / 상태 수선
103
+ - 정상 회사 루프로 복귀: `/harness-team`
@@ -54,7 +54,7 @@ Planner가 완료한 feature-list.json의 피처들을 최대 3개 팀이 병렬
54
54
 
55
55
  ```bash
56
56
  # progress.json에 mode=team 설정
57
- jq '.mode = "team" | .team_state.active_teams = 3 | .team_state.paused_at = null' .harness/progress.json > /tmp/progress_tmp.json && mv /tmp/progress_tmp.json .harness/progress.json
57
+ jq '.mode = "team" | .mode_decision.user_override = "team" | .team_state.active_teams = 3 | .team_state.paused_at = null' .harness/progress.json > /tmp/progress_tmp.json && mv /tmp/progress_tmp.json .harness/progress.json
58
58
 
59
59
  # Queue 초기화 또는 복구
60
60
  if [ ! -f .harness/actions/feature-queue.json ]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walwal-harness/cli",
3
- "version": "6.0.5",
3
+ "version": "6.1.0",
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"
@@ -0,0 +1,382 @@
1
+ #!/bin/bash
2
+ # conductor-tick.sh — document-driven company-loop router
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh"
7
+
8
+ PROJECT_ROOT="$(resolve_harness_root "${1:-.}")" || exit 0
9
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
10
+ CONFIG="$PROJECT_ROOT/.harness/config.json"
11
+ FEATURES="$PROJECT_ROOT/.harness/actions/feature-list.json"
12
+
13
+ [ -f "$PROGRESS" ] || exit 0
14
+ [ -f "$CONFIG" ] || exit 0
15
+ command -v jq >/dev/null 2>&1 || exit 0
16
+
17
+ candidate="${HARNESS_CONDUCTOR_CANDIDATE:-}"
18
+ if [ -z "$candidate" ]; then
19
+ candidate=$(jq -r '.next_agent // "null"' "$PROGRESS" 2>/dev/null || echo "null")
20
+ fi
21
+
22
+ current_agent=$(jq -r '.current_agent // "null"' "$PROGRESS")
23
+ agent_status=$(jq -r '.agent_status // "pending"' "$PROGRESS")
24
+ pipeline=$(jq -r '.pipeline // "null"' "$PROGRESS")
25
+ mode=$(jq -r '.mode // "auto"' "$PROGRESS")
26
+ user_override=$(jq -r '.mode_decision.user_override // "null"' "$PROGRESS")
27
+ requested_mode=$(jq -r '.service_ops.requested_mode // "null"' "$PROGRESS")
28
+ goal_adherence=$(jq -r '.goals.current_adherence // "null"' "$PROGRESS")
29
+ meetings_active_count=$(jq -r '(.meetings.active | length) // 0' "$PROGRESS")
30
+ cqo_verdict=$(jq -r '.cqo.sprint_verdict // "pending"' "$PROGRESS")
31
+ ops_report=$(jq -r '.service_ops.auto_retro.last_report // "null"' "$PROGRESS")
32
+ cto_hotfixes=$(jq -r '.cto.open_hotfixes // 0' "$PROGRESS")
33
+ ops_recommendations=$(jq -r '.service_ops.auto_retro.open_recommendations // 0' "$PROGRESS")
34
+ ops_alerts=$(jq -r '.service_ops.monitor.alerts_this_sprint // 0' "$PROGRESS")
35
+ open_incidents=$(jq -r '(.service_ops.incident.open | length) // 0' "$PROGRESS")
36
+ workflow_stage=$(jq -r '.workflow.stage // "goal-intake"' "$PROGRESS")
37
+ meeting_type=$(jq -r '.meetings.requested_type // "null"' "$PROGRESS")
38
+ meeting_reason=$(jq -r '.meetings.requested_reason // "null"' "$PROGRESS")
39
+ cqo_regressions=$(jq -r '.cqo.open_regressions // 0' "$PROGRESS")
40
+ cqo_axes_below=$(jq -r '(.cqo.axes_below_threshold | length) // 0' "$PROGRESS")
41
+ plan_status=$(jq -r '.artifacts["plan.md"].status // "pending"' "$PROGRESS")
42
+ feature_status=$(jq -r '.artifacts["feature-list.json"].status // "pending"' "$PROGRESS")
43
+ api_status=$(jq -r '.artifacts["api-contract.json"].status // "pending"' "$PROGRESS")
44
+ service_ops_drift=$(jq -r '.service_ops.drift_classification // "null"' "$PROGRESS")
45
+
46
+ set_progress() {
47
+ bash "$SCRIPT_DIR/harness-progress-set.sh" "$PROJECT_ROOT" "$1" >/dev/null
48
+ }
49
+
50
+ escape_json_string() {
51
+ jq -Rn --arg v "$1" '$v'
52
+ }
53
+
54
+ maybe_select_mode() {
55
+ local effective="$mode"
56
+ if [ "$user_override" != "null" ] && [ -n "$user_override" ]; then
57
+ effective="$user_override"
58
+ set_progress ".mode = \"$effective\" |
59
+ .mode_decision.owner = \"conductor\" |
60
+ .mode_decision.user_override = \"$effective\" |
61
+ .mode_decision.decided_at = (now | todate) |
62
+ .mode_decision.rationale = \"user override: $effective\""
63
+ echo "$effective"
64
+ return
65
+ fi
66
+ if [ "$mode" != "auto" ] || [ ! -f "$FEATURES" ]; then
67
+ echo "$mode"
68
+ return
69
+ fi
70
+
71
+ local feature_count ready_at_start critical_depth
72
+ feature_count=$(jq '.features | length // 0' "$FEATURES" 2>/dev/null || echo 0)
73
+ ready_at_start=$(jq '[.features[]? | select(((.depends_on // []) | length) == 0)] | length' "$FEATURES" 2>/dev/null || echo 0)
74
+ critical_depth=$(jq '
75
+ def feature($id): .features[]? | select(.id == $id);
76
+ def depth($id):
77
+ (feature($id) | .depends_on // []) as $deps
78
+ | if ($deps | length) == 0 then 1 else ([ $deps[] | depth(.) ] | max + 1) end;
79
+ ([.features[]?.id | depth(.)] | max) // 0
80
+ ' "$FEATURES" 2>/dev/null || echo 0)
81
+
82
+ local team_ready team_features team_depth selected rationale
83
+ team_ready=$(jq -r '.mode_selection.rules.force_team_when.ready_at_start_gte // 3' "$CONFIG")
84
+ team_features=$(jq -r '.mode_selection.rules.force_team_when.feature_count_gte // 6' "$CONFIG")
85
+ team_depth=$(jq -r '.mode_selection.rules.force_team_when.critical_path_depth_lte // 2' "$CONFIG")
86
+
87
+ if [ "$ready_at_start" -ge "$team_ready" ] && [ "$feature_count" -ge "$team_features" ] && [ "$critical_depth" -le "$team_depth" ]; then
88
+ selected="team"
89
+ else
90
+ selected=$(jq -r '.mode_selection.rules.tie_breaker // "team"' "$CONFIG")
91
+ fi
92
+ rationale="company-default: ready=${ready_at_start}, features=${feature_count}, depth=${critical_depth} -> ${selected}"
93
+ set_progress ".mode = \"$selected\" |
94
+ .mode_decision.owner = \"conductor\" |
95
+ .mode_decision.decided_at = (now | todate) |
96
+ .mode_decision.rationale = \"$rationale\""
97
+ echo "$selected"
98
+ }
99
+
100
+ infer_drift_classification() {
101
+ if [ "$service_ops_drift" != "null" ] && [ -n "$service_ops_drift" ]; then
102
+ echo "$service_ops_drift"
103
+ return
104
+ fi
105
+ if [ "$cqo_verdict" = "FAIL" ] || [ "${cqo_regressions:-0}" -gt 0 ] || [ "${cqo_axes_below:-0}" -gt 0 ]; then
106
+ echo "implementation_drift"
107
+ return
108
+ fi
109
+ if [ "$requested_mode" = "incident" ] || [ "${open_incidents:-0}" -gt 0 ] || [ "${ops_alerts:-0}" -gt 0 ]; then
110
+ echo "ops_drift"
111
+ return
112
+ fi
113
+ if [ "$goal_adherence" != "null" ] && awk "BEGIN {exit !($goal_adherence < 0.7)}"; then
114
+ if [ "$cqo_verdict" = "PASS" ] && [ "${open_incidents:-0}" -eq 0 ] && [ "${ops_alerts:-0}" -eq 0 ]; then
115
+ echo "planning_drift"
116
+ elif [ "$(jq -r '.goals.active_id // "null"' "$PROGRESS")" = "null" ]; then
117
+ echo "goal_drift"
118
+ else
119
+ echo "goal_drift"
120
+ fi
121
+ return
122
+ fi
123
+ echo "unknown"
124
+ }
125
+
126
+ meeting_stage_for_owner() {
127
+ case "$1" in
128
+ planner) echo "coo-planning" ;;
129
+ cto) echo "cto-review" ;;
130
+ cqo) echo "quality-review" ;;
131
+ service-ops) echo "ops-review" ;;
132
+ dispatcher) echo "goal-intake" ;;
133
+ *) echo "goal-intake" ;;
134
+ esac
135
+ }
136
+
137
+ load_meeting_decision() {
138
+ local decision
139
+ decision=$(bash "$SCRIPT_DIR/harness-meeting-doc.sh" "$PROJECT_ROOT" read-decision 2>/dev/null || true)
140
+ if [ -n "$decision" ]; then
141
+ echo "$decision"
142
+ else
143
+ jq -c '.meetings.decision // {}' "$PROGRESS"
144
+ fi
145
+ }
146
+
147
+ effective_mode="$(maybe_select_mode)"
148
+ drift_classification="$(infer_drift_classification)"
149
+
150
+ next="$candidate"
151
+ action="pass-through"
152
+ new_conductor_state="running"
153
+ new_workflow_stage="$workflow_stage"
154
+ meeting_filter=""
155
+ planner_filter=""
156
+ ops_filter=""
157
+ meeting_prepare=false
158
+
159
+ plan_artifacts_ready=false
160
+ if [ "$plan_status" != "pending" ] || [ "$feature_status" != "pending" ] || [ "$api_status" != "pending" ]; then
161
+ plan_artifacts_ready=true
162
+ fi
163
+
164
+ first_generator="generator-backend"
165
+ if [ "$pipeline" = "FE-ONLY" ]; then
166
+ first_generator="generator-frontend"
167
+ fi
168
+
169
+ if [ "$current_agent" = "dispatcher" ] && [ "$agent_status" = "completed" ]; then
170
+ next="meeting-manager"
171
+ action="convene:all-hands:goal-intake"
172
+ new_conductor_state="waiting_meeting"
173
+ new_workflow_stage="ceo-meeting"
174
+ meeting_prepare=true
175
+ meeting_filter="
176
+ .meetings.active = [\"meeting-manager\"] |
177
+ .meetings.requested_type = \"all-hands\" |
178
+ .meetings.requested_reason = \"goal-intake\" |
179
+ .meetings.decision = {
180
+ \"owner\": \"planner\",
181
+ \"action_type\": \"goal-alignment\",
182
+ \"rationale\": \"CEO intake always starts with COO alignment.\",
183
+ \"evidence\": [],
184
+ \"drift_classification\": null,
185
+ \"source_path\": null
186
+ }"
187
+
188
+ elif [ "$meetings_active_count" -gt 0 ] && [ "$current_agent" != "meeting-manager" ]; then
189
+ next="meeting-manager"
190
+ action="spawn:meeting-manager:${meeting_type}"
191
+ new_conductor_state="waiting_meeting"
192
+
193
+ elif [ "$requested_mode" != "null" ] && [ -n "$requested_mode" ] && [ "$current_agent" != "service-ops" ]; then
194
+ next="service-ops"
195
+ action="spawn:service-ops:${requested_mode}"
196
+
197
+ elif [ "$goal_adherence" != "null" ] && awk "BEGIN {exit !($goal_adherence < 0.7)}"; then
198
+ next="meeting-manager"
199
+ action="convene:spec-review:${drift_classification}"
200
+ new_conductor_state="waiting_meeting"
201
+ new_workflow_stage="goal-drift-review"
202
+ meeting_prepare=true
203
+ meeting_filter="
204
+ .meetings.active = [\"meeting-manager\"] |
205
+ .meetings.requested_type = \"spec-review\" |
206
+ .meetings.requested_reason = \"goal-drift\" |
207
+ .service_ops.drift_classification = \"$drift_classification\" |
208
+ .meetings.decision = {
209
+ \"owner\": null,
210
+ \"action_type\": null,
211
+ \"rationale\": null,
212
+ \"evidence\": [],
213
+ \"drift_classification\": \"$drift_classification\",
214
+ \"source_path\": null
215
+ }"
216
+
217
+ elif [ "$current_agent" = "meeting-manager" ] && [ "$agent_status" = "completed" ]; then
218
+ meeting_decision="$(load_meeting_decision)"
219
+ owner=$(jq -r '.owner // "null"' <<<"$meeting_decision")
220
+ action_type=$(jq -r '.action_type // "triage"' <<<"$meeting_decision")
221
+ rationale=$(jq -r '.rationale // ""' <<<"$meeting_decision")
222
+ evidence=$(jq -c '.evidence // []' <<<"$meeting_decision")
223
+ decision_drift=$(jq -r '.drift_classification // "unknown"' <<<"$meeting_decision")
224
+ source_path=$(jq -r '.source_path // "null"' <<<"$meeting_decision")
225
+
226
+ if [ "$owner" = "null" ] || [ -z "$owner" ]; then
227
+ case "$decision_drift" in
228
+ implementation_drift) owner="cto" ;;
229
+ planning_drift) owner="planner" ;;
230
+ ops_drift) owner="service-ops" ;;
231
+ goal_drift) owner="dispatcher" ;;
232
+ *)
233
+ if [ "$plan_artifacts_ready" = false ]; then owner="planner"; else owner="cto"; fi
234
+ ;;
235
+ esac
236
+ fi
237
+
238
+ next="$owner"
239
+ action="dispatch:${owner}:${action_type}"
240
+ new_workflow_stage="$(meeting_stage_for_owner "$owner")"
241
+ if [ "$owner" = "planner" ]; then
242
+ planner_filter=".planner.requested_mode = \"$action_type\""
243
+ fi
244
+ meeting_filter="
245
+ .meetings.active = [] |
246
+ .meetings.last_type = .meetings.requested_type |
247
+ .meetings.last_reason = .meetings.requested_reason |
248
+ .meetings.last_decision = \"$owner:$action_type\" |
249
+ .meetings.requested_type = null |
250
+ .meetings.requested_reason = null |
251
+ .meetings.decision = {
252
+ \"owner\": \"$owner\",
253
+ \"action_type\": \"$action_type\",
254
+ \"rationale\": $(escape_json_string "$rationale"),
255
+ \"evidence\": $evidence,
256
+ \"drift_classification\": $(escape_json_string "$decision_drift"),
257
+ \"source_path\": $(escape_json_string "$source_path")
258
+ }"
259
+
260
+ elif [ "$current_agent" = "planner" ] && [ "$agent_status" = "completed" ]; then
261
+ next="cto"
262
+ action="handoff:cto:plan-ready"
263
+ new_workflow_stage="cto-review"
264
+ planner_filter='
265
+ .planner.last_brief = (.planner.requested_mode // "goal-alignment") |
266
+ .planner.requested_mode = null'
267
+
268
+ elif [ "$current_agent" = "cto" ]; then
269
+ if [ "${cto_hotfixes:-0}" -gt 0 ] || [ "${ops_recommendations:-0}" -gt 0 ]; then
270
+ next="planner"
271
+ action="dispatch:planner:execution-plan"
272
+ new_workflow_stage="coo-replan"
273
+ planner_filter='.planner.requested_mode = "execution-plan"'
274
+ elif [ "$candidate" = "archive" ] && [ "$workflow_stage" = "ops-review" ]; then
275
+ next="archive"
276
+ action="archive"
277
+ new_conductor_state="completed"
278
+ new_workflow_stage="ops-monitoring"
279
+ else
280
+ next="$first_generator"
281
+ action="dispatch:workers:${first_generator}"
282
+ new_workflow_stage="implementation"
283
+ fi
284
+
285
+ elif [ "$candidate" = "archive" ]; then
286
+ if [[ "$current_agent" == evaluator-* ]] && [ "$cqo_verdict" = "pending" ]; then
287
+ next="cqo"
288
+ action="spawn:cqo"
289
+ new_workflow_stage="quality-review"
290
+ elif [ "$current_agent" = "cqo" ]; then
291
+ if [ "$cqo_verdict" = "FAIL" ] || [ "${cqo_regressions:-0}" -gt 0 ] || [ "${cqo_axes_below:-0}" -gt 0 ]; then
292
+ next="meeting-manager"
293
+ action="convene:spec-review:implementation_drift"
294
+ new_conductor_state="waiting_meeting"
295
+ new_workflow_stage="quality-review"
296
+ meeting_prepare=true
297
+ meeting_filter='
298
+ .meetings.active = ["meeting-manager"] |
299
+ .meetings.requested_type = "spec-review" |
300
+ .meetings.requested_reason = "quality-fail" |
301
+ .service_ops.drift_classification = "implementation_drift" |
302
+ .meetings.decision = {
303
+ "owner": null,
304
+ "action_type": null,
305
+ "rationale": null,
306
+ "evidence": [],
307
+ "drift_classification": "implementation_drift",
308
+ "source_path": null
309
+ }'
310
+ else
311
+ next="service-ops"
312
+ action="spawn:service-ops:auto-retro"
313
+ new_workflow_stage="ops-review"
314
+ ops_filter='.service_ops.requested_mode = "auto-retro"'
315
+ fi
316
+ elif [ "$cqo_verdict" != "pending" ] && [ "$ops_report" = "null" ]; then
317
+ next="service-ops"
318
+ action="spawn:service-ops:auto-retro"
319
+ new_workflow_stage="ops-review"
320
+ ops_filter='.service_ops.requested_mode = "auto-retro"'
321
+ elif [ "$current_agent" = "service-ops" ]; then
322
+ local_drift="$drift_classification"
323
+ if [ "$requested_mode" = "auto-retro" ] || [ "${open_incidents:-0}" -gt 0 ] || [ "${ops_alerts:-0}" -gt 0 ] || { [ "$goal_adherence" != "null" ] && awk "BEGIN {exit !($goal_adherence < 0.7)}"; }; then
324
+ next="meeting-manager"
325
+ action="convene:sprint-review:${local_drift}"
326
+ new_conductor_state="waiting_meeting"
327
+ new_workflow_stage="ops-review"
328
+ meeting_prepare=true
329
+ meeting_filter="
330
+ .meetings.active = [\"meeting-manager\"] |
331
+ .meetings.requested_type = \"sprint-review\" |
332
+ .meetings.requested_reason = \"ops-batch\" |
333
+ .service_ops.drift_classification = \"$local_drift\" |
334
+ .meetings.decision = {
335
+ \"owner\": null,
336
+ \"action_type\": null,
337
+ \"rationale\": null,
338
+ \"evidence\": [],
339
+ \"drift_classification\": \"$local_drift\",
340
+ \"source_path\": null
341
+ }"
342
+ else
343
+ next="archive"
344
+ action="archive"
345
+ new_conductor_state="completed"
346
+ new_workflow_stage="ops-monitoring"
347
+ ops_filter='.service_ops.requested_mode = null'
348
+ fi
349
+ fi
350
+ fi
351
+
352
+ filter=".conductor.state = \"$new_conductor_state\" |
353
+ .conductor.current_action = \"$action\" |
354
+ .conductor.last_tick = (now | todate) |
355
+ .conductor.tick_count = ((.conductor.tick_count // 0) + 1) |
356
+ .next_agent = \"$next\" |
357
+ .workflow.stage = \"$new_workflow_stage\" |
358
+ .workflow.last_transition = (now | todate) |
359
+ .workflow.last_reason = \"$action\""
360
+
361
+ if [ -n "$meeting_filter" ]; then
362
+ filter="$filter | $meeting_filter"
363
+ fi
364
+ if [ -n "$planner_filter" ]; then
365
+ filter="$filter | $planner_filter"
366
+ fi
367
+ if [ -n "$ops_filter" ]; then
368
+ filter="$filter | $ops_filter"
369
+ elif [ "$current_agent" = "service-ops" ] && [ "$next" != "service-ops" ]; then
370
+ filter="$filter | .service_ops.requested_mode = null"
371
+ fi
372
+ if [ "$next" = "meeting-manager" ] && [ "$workflow_stage" = "ops-review" ]; then
373
+ filter="$filter | .workflow.loop_count = ((.workflow.loop_count // 0) + 1)"
374
+ fi
375
+
376
+ set_progress "$filter"
377
+
378
+ if [ "$meeting_prepare" = true ] && [ "$next" = "meeting-manager" ]; then
379
+ bash "$SCRIPT_DIR/harness-meeting-doc.sh" "$PROJECT_ROOT" prepare >/dev/null 2>&1 || true
380
+ fi
381
+
382
+ echo "$next"
@@ -0,0 +1,123 @@
1
+ #!/bin/bash
2
+ # harness-archive.sh — 스프린트 종료 시 자동 아카이빙
3
+ #
4
+ # 동작:
5
+ # 1. .harness/actions/ 의 스프린트 산출물을 .harness/archive/D-NNN/S-NNN/ 로 이동
6
+ # 2. progress.json 의 sprint 상태를 초기화 (신규 dispatch 준비)
7
+ # 3. dispatch.id 가 없으면 counter++ 로 새 dispatch 시작
8
+ #
9
+ # 유지되는 파일 (이동하지 않음):
10
+ # - .harness/gotchas/**, .harness/conventions/**, .harness/ref/**
11
+ # - .harness/config.json, .harness/memory.md
12
+ # - .harness/progress.json (초기화만)
13
+ #
14
+ # 호출 주체: harness-next.sh (next_agent="archive" 도달 시 자동)
15
+
16
+ set -e
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19
+ source "$SCRIPT_DIR/lib/harness-render-progress.sh" 2>/dev/null || true
20
+
21
+ PROJECT_ROOT="${1:-.}"
22
+ PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"
23
+
24
+ PROGRESS="$PROJECT_ROOT/.harness/progress.json"
25
+ ACTIONS_DIR="$PROJECT_ROOT/.harness/actions"
26
+ ARCHIVE_ROOT="$PROJECT_ROOT/.harness/archive"
27
+
28
+ if [ ! -f "$PROGRESS" ]; then
29
+ echo "[archive] ERROR: progress.json not found" >&2
30
+ exit 1
31
+ fi
32
+
33
+ command -v jq >/dev/null 2>&1 || { echo "[archive] ERROR: jq required" >&2; exit 1; }
34
+
35
+ # ── Read current dispatch/sprint numbers ──
36
+ dispatch_counter=$(jq -r '.dispatch.counter // 0' "$PROGRESS")
37
+ dispatch_id=$(jq -r '.dispatch.id // empty' "$PROGRESS")
38
+ sprint_num=$(jq -r '.sprint.number // 0' "$PROGRESS")
39
+
40
+ # Ensure dispatch id exists (fallback for legacy progress.json without dispatch)
41
+ if [ -z "$dispatch_id" ] || [ "$dispatch_id" = "null" ]; then
42
+ if [ "$dispatch_counter" -lt 1 ]; then
43
+ dispatch_counter=1
44
+ fi
45
+ dispatch_id=$(printf 'D-%03d' "$dispatch_counter")
46
+ fi
47
+
48
+ sprint_id=$(printf 'S-%03d' "$sprint_num")
49
+ target_dir="$ARCHIVE_ROOT/$dispatch_id/$sprint_id"
50
+
51
+ echo ""
52
+ echo " ── Archive ────────────────────────────"
53
+ echo " Dispatch : $dispatch_id"
54
+ echo " Sprint : $sprint_id"
55
+ echo " Target : ${target_dir#$PROJECT_ROOT/}"
56
+
57
+ # ── Move actions/ contents into archive ──
58
+ if [ -d "$ACTIONS_DIR" ] && [ "$(ls -A "$ACTIONS_DIR" 2>/dev/null)" ]; then
59
+ mkdir -p "$target_dir"
60
+ moved=0
61
+ for f in "$ACTIONS_DIR"/*; do
62
+ [ -e "$f" ] || continue
63
+ name="$(basename "$f")"
64
+ if [ -e "$target_dir/$name" ]; then
65
+ # Collision — suffix with timestamp to avoid overwrite
66
+ ts=$(date +%Y%m%d-%H%M%S)
67
+ mv "$f" "$target_dir/${name%.}.${ts}"
68
+ else
69
+ mv "$f" "$target_dir/"
70
+ fi
71
+ moved=$((moved + 1))
72
+ done
73
+ echo " Moved : $moved file(s)"
74
+ else
75
+ echo " Moved : 0 file(s) (actions/ empty)"
76
+ fi
77
+
78
+ # ── Reset progress.json for next dispatch ──
79
+ # - sprint → init
80
+ # - agents → cleared
81
+ # - artifacts → pending
82
+ # - dispatch.id cleared (next dispatcher run will allocate new D-NNN)
83
+ # - failure → cleared
84
+ jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '
85
+ .pipeline = null
86
+ | .dispatch.id = null
87
+ | .sprint = { number: 0, status: "init", retry_count: 0 }
88
+ | .current_agent = null
89
+ | .agent_status = "pending"
90
+ | .completed_agents = []
91
+ | .next_agent = "dispatcher"
92
+ | .workflow = { stage: "goal-intake", last_transition: null, loop_count: 0, last_reason: null }
93
+ | .task_sessions.current = { agent: null, id: null, path: null, source: null }
94
+ | .failure = { agent: null, location: null, message: null, retry_target: null }
95
+ | .meetings.active = []
96
+ | .meetings.current_id = null
97
+ | .meetings.current_record_path = null
98
+ | .meetings.requested_type = null
99
+ | .meetings.requested_reason = null
100
+ | .meetings.last_type = null
101
+ | .meetings.last_reason = null
102
+ | .meetings.last_decision = null
103
+ | .meetings.decision = { owner: null, action_type: null, rationale: null, evidence: [], drift_classification: null, source_path: null }
104
+ | .service_ops.requested_mode = null
105
+ | .service_ops.drift_classification = null
106
+ | .service_ops.evidence = []
107
+ | (.artifacts // {}) as $a
108
+ | .artifacts = ($a | with_entries(
109
+ if (.value | type) == "object"
110
+ then .value = { status: "pending", updated_by: null, updated_at: null }
111
+ else .
112
+ end))
113
+ | .updated_at = $now
114
+ ' "$PROGRESS" > "${PROGRESS}.tmp" && mv "${PROGRESS}.tmp" "$PROGRESS"
115
+
116
+ # ── Clear handoff so next session starts fresh ──
117
+ HANDOFF="$PROJECT_ROOT/.harness/handoff.json"
118
+ echo '{}' > "$HANDOFF"
119
+
120
+ echo " Status : archived, progress reset"
121
+ echo ""
122
+ echo " ✓ 다음 요청은 새로운 dispatch 로 시작합니다."
123
+ echo ""