@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.
- package/CHANGELOG.md +42 -0
- package/commands/harness-solo.md +20 -20
- package/commands/harness-team.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +382 -0
- package/scripts/harness-archive.sh +123 -0
- package/scripts/harness-dashboard-up.sh +72 -0
- package/scripts/harness-dashboard.sh +509 -0
- package/scripts/harness-goal-init.sh +72 -0
- package/scripts/harness-goal-show.sh +37 -0
- package/scripts/harness-gotcha-memory.sh +348 -0
- package/scripts/harness-gotcha-register.sh +393 -0
- package/scripts/harness-meeting-doc.sh +153 -0
- package/scripts/harness-monitor.sh +398 -0
- package/scripts/harness-next.sh +577 -0
- package/scripts/harness-progress-set.sh +41 -0
- package/scripts/harness-prompt-history.sh +164 -0
- package/scripts/harness-queue-manager.sh +694 -0
- package/scripts/harness-session-start.sh +140 -0
- package/scripts/harness-statusline.sh +139 -0
- package/scripts/harness-task-session.sh +48 -0
- package/scripts/harness-tmux.sh +372 -0
- package/scripts/harness-user-prompt-submit.sh +123 -0
- package/scripts/init-agents-md.sh +337 -0
- package/scripts/init-ref-docs.sh +262 -0
- package/scripts/lib/harness-audit.sh +113 -0
- package/scripts/lib/harness-guardrail.sh +148 -0
- package/scripts/lib/harness-keywait.sh +57 -0
- package/scripts/lib/harness-render-progress.sh +346 -0
- package/scripts/scan-project.sh +367 -0
- package/skills/brainstorming/SKILL.md +4 -4
- package/skills/conductor/SKILL.md +12 -16
- package/skills/cqo/SKILL.md +7 -0
- package/skills/cto/SKILL.md +7 -0
- package/skills/dispatcher/SKILL.md +17 -28
- package/skills/evaluator-code-quality/SKILL.md +1 -1
- package/skills/evaluator-functional/SKILL.md +1 -1
- package/skills/evaluator-visual/SKILL.md +1 -1
- package/skills/generator-backend/SKILL.md +1 -1
- package/skills/generator-frontend/SKILL.md +1 -1
- package/skills/meeting-manager/SKILL.md +33 -1
- package/skills/planner/SKILL.md +23 -9
- 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
|
package/commands/harness-solo.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
docmeta:
|
|
3
3
|
id: harness-solo
|
|
4
|
-
title: /harness-solo —
|
|
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 —
|
|
27
|
+
# /harness-solo — 비상용 Single-Agent Fallback (v6.0+)
|
|
28
28
|
|
|
29
|
-
> v6.0
|
|
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
|
-
|
|
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"** →
|
|
79
|
-
- **next_agent = "generator-backend"** →
|
|
80
|
-
- **next_agent = "generator-frontend"** →
|
|
81
|
-
- **next_agent = "evaluator-functional"** →
|
|
82
|
-
- **next_agent = "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
|
-
각 에이전트는 완료 즉시
|
|
86
|
+
각 에이전트는 완료 즉시 **내부 handoff(`harness-next.sh` 또는 SessionStart hook)** 로 다음 에이전트에 넘겨집니다. Owner가 `/harness-next` 를 직접 다룬다는 전제는 6.0 회사 모델과 맞지 않습니다.
|
|
87
87
|
|
|
88
88
|
**예외 — 사용자 승인 게이트가 있는 단계:**
|
|
89
|
-
- **Brainstorming**: brainstorm-spec.md 완성 후 사용자 승인 대기 → 승인 시
|
|
90
|
-
- **Planner**: plan.md / api-contract.json 완성 후 사용자 승인 대기 → 승인 시
|
|
89
|
+
- **Brainstorming**: brainstorm-spec.md 완성 후 사용자 승인 대기 → 승인 시 내부 handoff
|
|
90
|
+
- **Planner**: plan.md / api-contract.json 완성 후 사용자 승인 대기 → 승인 시 내부 handoff
|
|
91
91
|
|
|
92
|
-
흐름이
|
|
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
|
-
##
|
|
98
|
+
## 사용 권장 상황
|
|
99
99
|
|
|
100
100
|
- 프롬프트로 각 에이전트 순차 실행
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
101
|
+
- 내부 handoff 스크립트 장애 시 응급 복구
|
|
102
|
+
- 아주 짧은 수동 hotfix / 상태 수선
|
|
103
|
+
- 정상 회사 루프로 복귀: `/harness-team`
|
package/commands/harness-team.md
CHANGED
|
@@ -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
|
|
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 ""
|