@walwal-harness/cli 5.9.5 → 6.0.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 +105 -0
- package/README.md +34 -11
- package/assets/templates/config.json +29 -0
- package/assets/templates/progress.json.template +8 -2
- package/bin/init.js +147 -9
- package/commands/harness-next.md +3 -1
- package/commands/harness-solo.md +20 -4
- package/commands/harness-team.md +61 -5
- package/gotchas/generator-backend-laravel.md +85 -0
- package/package.json +11 -3
- package/scripts/harness-dashboard-up.sh +72 -0
- package/scripts/harness-dashboard.sh +41 -10
- package/scripts/harness-goal-init.sh +72 -0
- package/scripts/harness-goal-show.sh +37 -0
- package/scripts/harness-next.sh +10 -11
- package/scripts/harness-queue-manager.sh +123 -1
- package/skills/conductor/SKILL.md +234 -0
- package/skills/cqo/SKILL.md +138 -0
- package/skills/cto/SKILL.md +133 -0
- package/skills/dispatcher/SKILL.md +10 -17
- package/skills/dispatcher/persona-ceo.md +168 -0
- package/skills/evaluator-architecture/SKILL.md +173 -0
- package/skills/evaluator-security/SKILL.md +172 -0
- package/skills/generator-designer/SKILL.md +219 -0
- package/skills/generator-devops/SKILL.md +201 -0
- package/skills/meeting-manager/SKILL.md +206 -0
- package/skills/planner/hr-onboard.md +134 -0
- package/skills/planner/hr-recruit.md +99 -0
- package/skills/planner/persona-coo-hr.md +165 -0
- package/skills/service-ops/SKILL.md +255 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Production harness for AI agent engineering — Solo/Team mode, Planner, Generator(BE/FE), Evaluator chain (Code-Quality → Functional → Visual
|
|
3
|
+
"version": "6.0.0",
|
|
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"
|
|
7
7
|
},
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
"planner",
|
|
25
25
|
"generator",
|
|
26
26
|
"evaluator",
|
|
27
|
+
"nexus",
|
|
28
|
+
"ceo",
|
|
29
|
+
"cto",
|
|
30
|
+
"cqo",
|
|
31
|
+
"conductor",
|
|
32
|
+
"meeting-manager",
|
|
33
|
+
"service-ops",
|
|
27
34
|
"nestjs",
|
|
28
35
|
"react",
|
|
29
36
|
"flutter",
|
|
@@ -45,6 +52,7 @@
|
|
|
45
52
|
"scripts/",
|
|
46
53
|
"commands/",
|
|
47
54
|
"assets/",
|
|
48
|
-
"gotchas/"
|
|
55
|
+
"gotchas/",
|
|
56
|
+
"CHANGELOG.md"
|
|
49
57
|
]
|
|
50
58
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# walwal-harness — Brick Office dashboard launcher
|
|
3
|
+
#
|
|
4
|
+
# 처음 쓰는 사용자도 한 줄로 대시보드를 띄워볼 수 있게 만든 helper.
|
|
5
|
+
# walwal-harness CLI 패키지에는 R3F + Next.js 무게 때문에 dashboard 가 미포함이고,
|
|
6
|
+
# 본 스크립트가 git sparse-checkout 으로 apps/harness-dashboard/ 만 가져와서
|
|
7
|
+
# 사용자 프로젝트 외부 (~/.walwal-harness/dashboard) 에 격리 설치 후 dev 실행한다.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash scripts/harness-dashboard-up.sh # install + dev (port 3001)
|
|
11
|
+
# bash scripts/harness-dashboard-up.sh --port 3050 # override port
|
|
12
|
+
# HARNESS_ROOT=/path/to/project bash ... # explicit harness root
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
REPO_URL="${WALWAL_HARNESS_REPO:-https://github.com/30eggis/walwal-harness.git}"
|
|
16
|
+
DASHBOARD_PATH="apps/harness-dashboard"
|
|
17
|
+
LOCAL_DIR="${HOME}/.walwal-harness/dashboard"
|
|
18
|
+
PORT="${PORT:-3001}"
|
|
19
|
+
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--port) PORT="$2"; shift 2 ;;
|
|
23
|
+
--reinstall) rm -rf "$LOCAL_DIR"; shift ;;
|
|
24
|
+
--help|-h)
|
|
25
|
+
sed -n '2,16p' "$0"
|
|
26
|
+
exit 0 ;;
|
|
27
|
+
*) echo "[brick-office] unknown arg: $1" >&2; exit 2 ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
if [[ -z "${HARNESS_ROOT:-}" ]]; then
|
|
32
|
+
HARNESS_ROOT="$(pwd)"
|
|
33
|
+
fi
|
|
34
|
+
if [[ ! -d "${HARNESS_ROOT}/.harness" ]]; then
|
|
35
|
+
echo "[brick-office] FAIL: ${HARNESS_ROOT} 에 .harness/ 가 없습니다."
|
|
36
|
+
echo " 먼저 'npx walwal-harness' 로 초기화하세요."
|
|
37
|
+
exit 3
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "[brick-office] HARNESS_ROOT = ${HARNESS_ROOT}"
|
|
41
|
+
echo "[brick-office] dashboard 격리 경로 = ${LOCAL_DIR}"
|
|
42
|
+
|
|
43
|
+
if [[ ! -d "${LOCAL_DIR}/${DASHBOARD_PATH}" ]]; then
|
|
44
|
+
echo "[brick-office] 첫 실행 — git sparse-checkout 으로 dashboard 만 가져옵니다 (~5MB)."
|
|
45
|
+
mkdir -p "${LOCAL_DIR}"
|
|
46
|
+
cd "${LOCAL_DIR}"
|
|
47
|
+
git init -q
|
|
48
|
+
git remote add origin "${REPO_URL}" 2>/dev/null || true
|
|
49
|
+
git config core.sparseCheckout true
|
|
50
|
+
echo "${DASHBOARD_PATH}/" > .git/info/sparse-checkout
|
|
51
|
+
git fetch --depth=1 origin main -q
|
|
52
|
+
git checkout main -q
|
|
53
|
+
echo "[brick-office] 다운로드 완료."
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
cd "${LOCAL_DIR}/${DASHBOARD_PATH}"
|
|
57
|
+
|
|
58
|
+
if [[ ! -d node_modules ]]; then
|
|
59
|
+
echo "[brick-office] npm install (한 번만 실행, ~30s)..."
|
|
60
|
+
npm install --no-audit --no-fund --loglevel=error
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
echo ""
|
|
64
|
+
echo "╔══════════════════════════════════════════════════════════╗"
|
|
65
|
+
echo "║ Brick Office — http://localhost:${PORT}"
|
|
66
|
+
echo "║ ║"
|
|
67
|
+
echo "║ HARNESS_ROOT 이 가리키는 프로젝트의 .harness/ 상태를 ║"
|
|
68
|
+
echo "║ 실시간 (SSE) 시각화합니다. Ctrl+C 로 종료. ║"
|
|
69
|
+
echo "╚══════════════════════════════════════════════════════════╝"
|
|
70
|
+
echo ""
|
|
71
|
+
|
|
72
|
+
HARNESS_ROOT="${HARNESS_ROOT}" npm run dev:dashboard -- -p "${PORT}"
|
|
@@ -236,14 +236,23 @@ render_team_queue_summary() {
|
|
|
236
236
|
failed=$(jq '.queue.failed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
237
237
|
|
|
238
238
|
# passed 는 queue.passed ∪ feature-list.json 의 self-passed 를 dedup 합집합으로 계산
|
|
239
|
-
#
|
|
239
|
+
# total 도 feature-list IDs ∪ queue 전체 IDs 합집합으로 계산 — 새 sprint feature 가
|
|
240
|
+
# queue 에만 추가되고 feature-list 에 누락되어도 누락 없이 카운트 (예: F-800+ Sprint 8)
|
|
240
241
|
if [ -f "$FEATURES" ]; then
|
|
241
242
|
passed=$(jq -r --slurpfile q "$QUEUE" '
|
|
242
243
|
($q[0].queue.passed // []) as $qp |
|
|
243
244
|
([.features[] | select((.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) | .id]) as $sp |
|
|
244
245
|
($qp + $sp | unique | length)
|
|
245
246
|
' "$FEATURES" 2>/dev/null || echo 0)
|
|
246
|
-
total=$(jq
|
|
247
|
+
total=$(jq -r --slurpfile q "$QUEUE" '
|
|
248
|
+
([.features[].id]) as $fl |
|
|
249
|
+
(($q[0].queue.ready // []) +
|
|
250
|
+
($q[0].queue.passed // []) +
|
|
251
|
+
(($q[0].queue.failed // []) | if type == "object" then keys else . end) +
|
|
252
|
+
(($q[0].queue.in_progress // {}) | keys) +
|
|
253
|
+
(($q[0].queue.blocked // {}) | keys)) as $qids |
|
|
254
|
+
($fl + $qids | unique | length)
|
|
255
|
+
' "$FEATURES" 2>/dev/null || echo 0)
|
|
247
256
|
else
|
|
248
257
|
passed=$(jq '.queue.passed | length' "$QUEUE" 2>/dev/null || echo 0)
|
|
249
258
|
total=$((ready + blocked + in_prog + passed + failed))
|
|
@@ -260,6 +269,24 @@ render_team_queue_summary() {
|
|
|
260
269
|
for ((i=0; i<empty; i++)); do bar+="░"; done
|
|
261
270
|
|
|
262
271
|
echo -e " ${bar} ${passed}/${total} (${pct}%) R:${GREEN}${ready}${RESET} B:${YELLOW}${blocked}${RESET} P:${CYAN}${in_prog}${RESET} ${GREEN}✓${passed}${RESET} ${RED}✗${failed}${RESET}"
|
|
272
|
+
|
|
273
|
+
# Integrity warn — 큐에 있지만 feature-list 에 없는 orphan feature 가 있으면 경고.
|
|
274
|
+
# 발생 원인: Lead 가 jq 로 큐 직접 편집 (canonical path = enqueue 명령 사용).
|
|
275
|
+
if [ -f "$FEATURES" ]; then
|
|
276
|
+
local orphan_count
|
|
277
|
+
orphan_count=$(jq -r --slurpfile q "$QUEUE" '
|
|
278
|
+
([.features[].id]) as $fl |
|
|
279
|
+
(($q[0].queue.ready // []) +
|
|
280
|
+
($q[0].queue.passed // []) +
|
|
281
|
+
(($q[0].queue.failed // []) | if type == "object" then keys else . end) +
|
|
282
|
+
(($q[0].queue.in_progress // {}) | keys) +
|
|
283
|
+
(($q[0].queue.blocked // {}) | keys)) as $qids |
|
|
284
|
+
($qids - $fl) | unique | length
|
|
285
|
+
' "$FEATURES" 2>/dev/null || echo 0)
|
|
286
|
+
if [ "${orphan_count:-0}" -gt 0 ]; then
|
|
287
|
+
echo -e " ${YELLOW}⚠ ${orphan_count} feature(s) in queue but not in feature-list.json${RESET} ${DIM}(use: queue-manager enqueue)${RESET}"
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
263
290
|
}
|
|
264
291
|
|
|
265
292
|
render_team_status() {
|
|
@@ -313,19 +340,23 @@ render_team_features() {
|
|
|
313
340
|
echo ""
|
|
314
341
|
echo -e "${BOLD}Features${RESET}"
|
|
315
342
|
|
|
343
|
+
# feature-list IDs ∪ queue 전체 IDs 합집합을 iterate.
|
|
344
|
+
# feature-list 에 등록 안 된 queue-only feature 는 name="(queue)" 로 표시.
|
|
316
345
|
jq -r --slurpfile q "$QUEUE" '
|
|
317
346
|
($q[0].queue.passed // []) as $passed |
|
|
318
|
-
($q[0].queue.failed // []) as $
|
|
347
|
+
($q[0].queue.failed // []) as $failed_raw |
|
|
348
|
+
(if ($failed_raw | type) == "object" then ($failed_raw | keys) else $failed_raw end) as $failed |
|
|
319
349
|
($q[0].queue.ready // []) as $ready |
|
|
320
350
|
($q[0].queue.in_progress // {}) as $prog |
|
|
321
351
|
($q[0].queue.blocked // {}) as $blocked |
|
|
322
|
-
.features[] |
|
|
323
|
-
.id as $
|
|
324
|
-
(.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
(
|
|
352
|
+
([.features[] | {id: .id, name: (.name // .title // .description // "?"), passes: (.passes // [])}]) as $fl |
|
|
353
|
+
([$fl[] | {(.id): .}] | add // {}) as $fl_map |
|
|
354
|
+
(([$fl[].id]) + $ready + $passed + $failed + ($prog | keys) + ($blocked | keys) | unique) as $all_ids |
|
|
355
|
+
$all_ids[] |
|
|
356
|
+
. as $fid |
|
|
357
|
+
($fl_map[$fid] // {name: "(queue)", passes: []}) as $f |
|
|
358
|
+
($f.name | if length > 18 then .[0:16] + ".." else . end) as $fname |
|
|
359
|
+
(($f.passes // []) | any(. == "evaluator-functional" or . == "evaluator-visual" or . == "evaluator-code-quality")) as $self_passed |
|
|
329
360
|
(if ($fid | IN($passed[])) or $self_passed then "P"
|
|
330
361
|
elif $prog[$fid] then "I|\($prog[$fid].team)|\($prog[$fid].phase)"
|
|
331
362
|
elif ($fid | IN($failed[])) then "F"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# harness-goal-init.sh — 새 GOAL 발급 (CEO=Dispatcher 전용)
|
|
3
|
+
# 사용법: bash scripts/harness-goal-init.sh "<title>"
|
|
4
|
+
#
|
|
5
|
+
# 동작:
|
|
6
|
+
# 1. .harness/actions/goals.md 가 없으면 template로 생성
|
|
7
|
+
# 2. 다음 GOAL ID(G-N) 발급 → progress.json.goals.list 에 등록
|
|
8
|
+
# 3. progress.json.goals.active_id 갱신
|
|
9
|
+
# 4. CTO 협의 미완료 상태(cto_feasibility=null, owner_confirmed=false)
|
|
10
|
+
#
|
|
11
|
+
# 후속:
|
|
12
|
+
# - CTO는 cto_feasibility 의견을 협의 후 같은 항목에 채워줌
|
|
13
|
+
# - Owner 최종 확인 시 dispatcher 가 owner_confirmed=true 로 갱신
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
17
|
+
GOAL_FILE="$ROOT/.harness/actions/goals.md"
|
|
18
|
+
TEMPLATE="$ROOT/.harness/actions/goals.md.template"
|
|
19
|
+
PROGRESS="$ROOT/.harness/progress.json"
|
|
20
|
+
|
|
21
|
+
TITLE="${1:-}"
|
|
22
|
+
if [ -z "$TITLE" ]; then
|
|
23
|
+
echo "usage: $0 \"<title>\"" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
mkdir -p "$ROOT/.harness/actions"
|
|
28
|
+
|
|
29
|
+
# 1. 파일이 없으면 template 복사 후 ISO 시각 채움
|
|
30
|
+
if [ ! -f "$GOAL_FILE" ]; then
|
|
31
|
+
if [ ! -f "$TEMPLATE" ]; then
|
|
32
|
+
echo "template missing: $TEMPLATE" >&2; exit 1
|
|
33
|
+
fi
|
|
34
|
+
ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
35
|
+
sed "s|<ISO>|$ISO|g" "$TEMPLATE" > "$GOAL_FILE"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# 2. 다음 GOAL ID 계산
|
|
39
|
+
LAST_N=$(jq -r '.goals.list | map(.id // "G-0") | map(sub("G-"; "")) | map(tonumber? // 0) | max // 0' "$PROGRESS" 2>/dev/null || echo 0)
|
|
40
|
+
NEXT_N=$((LAST_N + 1))
|
|
41
|
+
GOAL_ID="G-${NEXT_N}"
|
|
42
|
+
ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
43
|
+
|
|
44
|
+
# 3. progress.json 에 등록 (partial update)
|
|
45
|
+
jq --arg id "$GOAL_ID" --arg title "$TITLE" --arg iso "$ISO" '
|
|
46
|
+
.goals.list += [{
|
|
47
|
+
"id": $id,
|
|
48
|
+
"title": $title,
|
|
49
|
+
"status": "draft",
|
|
50
|
+
"owner_confirmed": false,
|
|
51
|
+
"cto_feasibility": null,
|
|
52
|
+
"created_at": $iso
|
|
53
|
+
}] |
|
|
54
|
+
.goals.active_id = $id |
|
|
55
|
+
.updated_at = $iso
|
|
56
|
+
' "$PROGRESS" > "$PROGRESS.tmp" && mv "$PROGRESS.tmp" "$PROGRESS"
|
|
57
|
+
|
|
58
|
+
# 4. goals.md 에 GOAL 섹션 append (CEO가 이후 본문 채움)
|
|
59
|
+
cat >> "$GOAL_FILE" <<EOF
|
|
60
|
+
|
|
61
|
+
## $GOAL_ID — $TITLE
|
|
62
|
+
status: draft
|
|
63
|
+
owner_confirmed: false
|
|
64
|
+
cto_feasibility: null
|
|
65
|
+
created_at: $ISO
|
|
66
|
+
|
|
67
|
+
(CEO가 success_metrics·deadline·kpis·runbook 채움)
|
|
68
|
+
EOF
|
|
69
|
+
|
|
70
|
+
echo "$GOAL_ID 발급 완료: $TITLE"
|
|
71
|
+
echo "→ $GOAL_FILE 에서 본문 작성"
|
|
72
|
+
echo "→ CTO 협의 후 owner 확정 시 owner_confirmed=true"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# harness-goal-show.sh — 현재 active GOAL + 적합도 요약 출력
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
6
|
+
PROGRESS="$ROOT/.harness/progress.json"
|
|
7
|
+
GOAL_FILE="$ROOT/.harness/actions/goals.md"
|
|
8
|
+
|
|
9
|
+
if [ ! -f "$PROGRESS" ]; then
|
|
10
|
+
echo "progress.json 없음. bash init.sh 먼저 실행." >&2; exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
ACTIVE_ID=$(jq -r '.goals.active_id // ""' "$PROGRESS")
|
|
14
|
+
ADHERENCE=$(jq -r '.goals.current_adherence // "n/a"' "$PROGRESS")
|
|
15
|
+
|
|
16
|
+
if [ -z "$ACTIVE_ID" ]; then
|
|
17
|
+
echo "Active GOAL 없음. CEO(Dispatcher)가 첫 발화에서 발급."
|
|
18
|
+
echo " bash scripts/harness-goal-init.sh \"<title>\""
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
echo "Active GOAL: $ACTIVE_ID"
|
|
23
|
+
echo "Adherence: $ADHERENCE"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# progress.json 의 GOAL 메타 출력
|
|
27
|
+
jq -r --arg id "$ACTIVE_ID" '
|
|
28
|
+
.goals.list[] | select(.id == $id) |
|
|
29
|
+
"title: \(.title)",
|
|
30
|
+
"status: \(.status)",
|
|
31
|
+
"owner_confirmed: \(.owner_confirmed)",
|
|
32
|
+
"cto_feasibility: \(.cto_feasibility // "pending")",
|
|
33
|
+
"created_at: \(.created_at // "?")"
|
|
34
|
+
' "$PROGRESS"
|
|
35
|
+
|
|
36
|
+
echo ""
|
|
37
|
+
echo "본문: $GOAL_FILE 에서 ## $ACTIVE_ID 섹션 참조"
|
package/scripts/harness-next.sh
CHANGED
|
@@ -415,25 +415,24 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
|
|
|
415
415
|
# ── Collect artifacts ──
|
|
416
416
|
FEATURE_LIST="$PROJECT_ROOT/.harness/actions/feature-list.json"
|
|
417
417
|
|
|
418
|
-
|
|
418
|
+
artifacts_ready=()
|
|
419
419
|
for f in plan.md feature-list.json api-contract.json sprint-contract.md evaluation-functional.md evaluation-visual.md; do
|
|
420
420
|
if [ -f "$PROJECT_ROOT/.harness/actions/$f" ]; then
|
|
421
421
|
artifacts_ready+=("$f")
|
|
422
422
|
fi
|
|
423
423
|
done
|
|
424
|
-
local artifacts_json
|
|
425
424
|
artifacts_json=$(printf '%s\n' "${artifacts_ready[@]}" | jq -R . | jq -s .)
|
|
426
425
|
|
|
427
426
|
# Collect focus features (incomplete ones)
|
|
428
|
-
|
|
427
|
+
focus_features="[]"
|
|
429
428
|
if [ -f "$FEATURE_LIST" ]; then
|
|
430
429
|
focus_features=$(jq '[.features[]? | select(.passes == null or (.passes | length) == 0 or ((.passes // []) | map(select(. == "evaluator-functional")) | length == 0)) | .id] | .[0:5]' "$FEATURE_LIST" 2>/dev/null || echo "[]")
|
|
431
430
|
fi
|
|
432
431
|
|
|
433
432
|
# ── Regression data ──
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
433
|
+
regression_source="null"
|
|
434
|
+
prev_sprint=$((sprint_num - 1))
|
|
435
|
+
prev_archive="$PROJECT_ROOT/.harness/archive/sprint-$(printf '%03d' $prev_sprint)"
|
|
437
436
|
if [ "$prev_sprint" -ge 1 ] && [ -d "$prev_archive" ]; then
|
|
438
437
|
if [ -f "$prev_archive/feature-list.json" ]; then
|
|
439
438
|
regression_source=$(jq '{
|
|
@@ -445,8 +444,8 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
|
|
|
445
444
|
fi
|
|
446
445
|
|
|
447
446
|
# ── Eval-specific config ──
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
eval_config="null"
|
|
448
|
+
cross_validation_data="null"
|
|
450
449
|
case "$next_agent" in
|
|
451
450
|
evaluator-*)
|
|
452
451
|
eval_config=$(jq '{
|
|
@@ -462,15 +461,15 @@ if [ "$next_agent" != "null" ] && [ "$next_agent" != "archive" ] && [ "$agent_st
|
|
|
462
461
|
esac
|
|
463
462
|
|
|
464
463
|
# ── Cross-Validation (chain: code-quality → functional → visual) ──
|
|
465
|
-
|
|
464
|
+
cross_validation_from_code_quality="null"
|
|
466
465
|
if [ "$next_agent" = "evaluator-functional" ] || [ "$next_agent" = "evaluator-visual" ]; then
|
|
467
|
-
|
|
466
|
+
cq_eval="$PROJECT_ROOT/.harness/actions/evaluation-code-quality.md"
|
|
468
467
|
if [ -f "$cq_eval" ]; then
|
|
469
468
|
cross_validation_from_code_quality=$(sed -n '/```json/,/```/p' "$cq_eval" | tail -n +2 | head -n -1 | jq 'select(.evaluator == "code-quality" or .cross_validation_from_code_quality)' 2>/dev/null || echo "null")
|
|
470
469
|
fi
|
|
471
470
|
fi
|
|
472
471
|
if [ "$next_agent" = "evaluator-visual" ]; then
|
|
473
|
-
|
|
472
|
+
func_eval="$PROJECT_ROOT/.harness/actions/evaluation-functional.md"
|
|
474
473
|
if [ -f "$func_eval" ]; then
|
|
475
474
|
cross_validation_data=$(sed -n '/```json/,/```/p' "$func_eval" | tail -n +2 | head -n -1 | jq 'select(.evaluator == "functional")' 2>/dev/null || echo "null")
|
|
476
475
|
fi
|
|
@@ -541,6 +541,126 @@ cmd_resume_probe() {
|
|
|
541
541
|
exit 1
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
# ══════════════════════════════════════════
|
|
545
|
+
# enqueue — Append feature to feature-list.json AND insert into queue (canonical path).
|
|
546
|
+
#
|
|
547
|
+
# Lead 가 ad-hoc 핫픽스 / 새 sprint feature 를 추가할 때 jq 로 큐를 직접 편집하면
|
|
548
|
+
# feature-list.json 과 큐가 갈라진다 (실제 사고: F-716, F-800~F-806 이 큐에만 등록).
|
|
549
|
+
# 이 명령은 feature-list append → queue insert (deps 미해소면 blocked) 를 한 트랜잭션으로 처리.
|
|
550
|
+
#
|
|
551
|
+
# Usage: enqueue <feature-json-file> # 파일 경로
|
|
552
|
+
# enqueue - # stdin 으로 JSON
|
|
553
|
+
#
|
|
554
|
+
# 입력 JSON 최소 필드: {id, title, sprint, depends_on?, layer?, service?, acceptance_criteria?}
|
|
555
|
+
# ══════════════════════════════════════════
|
|
556
|
+
cmd_enqueue() {
|
|
557
|
+
local input="${1:-}"
|
|
558
|
+
if [ -z "$input" ]; then
|
|
559
|
+
echo "[queue] enqueue: feature JSON 파일 경로 또는 '-' (stdin) 필요" >&2
|
|
560
|
+
exit 1
|
|
561
|
+
fi
|
|
562
|
+
if [ ! -f "$FEATURES" ]; then echo "[queue] feature-list.json not found." >&2; exit 1; fi
|
|
563
|
+
if [ ! -f "$QUEUE" ]; then echo "[queue] feature-queue.json not found. init 먼저." >&2; exit 1; fi
|
|
564
|
+
|
|
565
|
+
local feat_json
|
|
566
|
+
if [ "$input" = "-" ]; then
|
|
567
|
+
feat_json=$(cat)
|
|
568
|
+
else
|
|
569
|
+
feat_json=$(cat "$input")
|
|
570
|
+
fi
|
|
571
|
+
|
|
572
|
+
if ! echo "$feat_json" | jq -e '.id and .title and .sprint' >/dev/null 2>&1; then
|
|
573
|
+
echo "[queue] enqueue: id/title/sprint 필수 필드 누락" >&2
|
|
574
|
+
exit 1
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
local fid
|
|
578
|
+
fid=$(echo "$feat_json" | jq -r '.id')
|
|
579
|
+
|
|
580
|
+
acquire_queue_lock
|
|
581
|
+
|
|
582
|
+
if jq -e --arg id "$fid" '.features[] | select(.id == $id)' "$FEATURES" >/dev/null 2>&1; then
|
|
583
|
+
release_queue_lock
|
|
584
|
+
echo "[queue] enqueue: $fid 이 feature-list.json 에 이미 존재" >&2
|
|
585
|
+
exit 1
|
|
586
|
+
fi
|
|
587
|
+
|
|
588
|
+
if jq -e --arg id "$fid" '
|
|
589
|
+
($id | IN(.queue.ready[]?,.queue.passed[]?)) or
|
|
590
|
+
(.queue.in_progress[$id]?) or (.queue.blocked[$id]?) or
|
|
591
|
+
(.queue.failed[$id]? // (.queue.failed | type == "array" and ($id | IN(.queue.failed[]?))))
|
|
592
|
+
' "$QUEUE" >/dev/null 2>&1; then
|
|
593
|
+
release_queue_lock
|
|
594
|
+
echo "[queue] enqueue: $fid 이 큐에 이미 존재" >&2
|
|
595
|
+
exit 1
|
|
596
|
+
fi
|
|
597
|
+
|
|
598
|
+
local tmp_features="${FEATURES}.tmp.$$"
|
|
599
|
+
if ! jq --argjson new "$feat_json" '.features += [$new]' "$FEATURES" > "$tmp_features" 2>/dev/null; then
|
|
600
|
+
rm -f "$tmp_features"; release_queue_lock
|
|
601
|
+
echo "[queue] enqueue: feature-list.json 업데이트 실패" >&2; exit 1
|
|
602
|
+
fi
|
|
603
|
+
mv "$tmp_features" "$FEATURES"
|
|
604
|
+
|
|
605
|
+
local deps unmet
|
|
606
|
+
deps=$(echo "$feat_json" | jq -c '.depends_on // []')
|
|
607
|
+
unmet=$(jq --argjson d "$deps" '
|
|
608
|
+
($d - (.queue.passed // [])) | length
|
|
609
|
+
' "$QUEUE" 2>/dev/null || echo 0)
|
|
610
|
+
|
|
611
|
+
local tmp_queue="${QUEUE}.tmp.$$"
|
|
612
|
+
if [ "${unmet:-0}" -gt 0 ]; then
|
|
613
|
+
jq --arg id "$fid" --argjson deps "$deps" '.queue.blocked[$id] = $deps' "$QUEUE" > "$tmp_queue" 2>/dev/null
|
|
614
|
+
else
|
|
615
|
+
jq --arg id "$fid" '.queue.ready += [$id]' "$QUEUE" > "$tmp_queue" 2>/dev/null
|
|
616
|
+
fi
|
|
617
|
+
if [ ! -s "$tmp_queue" ]; then
|
|
618
|
+
rm -f "$tmp_queue"; release_queue_lock
|
|
619
|
+
echo "[queue] enqueue: feature-queue.json 업데이트 실패" >&2; exit 1
|
|
620
|
+
fi
|
|
621
|
+
mv "$tmp_queue" "$QUEUE"
|
|
622
|
+
|
|
623
|
+
release_queue_lock
|
|
624
|
+
|
|
625
|
+
if [ "${unmet:-0}" -gt 0 ]; then
|
|
626
|
+
echo "[queue] enqueue: $fid → blocked (unmet deps: ${unmet})"
|
|
627
|
+
else
|
|
628
|
+
echo "[queue] enqueue: $fid → ready"
|
|
629
|
+
fi
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
# ══════════════════════════════════════════
|
|
633
|
+
# integrity — feature-list ↔ queue divergence 검사.
|
|
634
|
+
# 큐에 있지만 feature-list 에 없는 feature ID 를 출력. 0건이면 종료코드 0.
|
|
635
|
+
# ══════════════════════════════════════════
|
|
636
|
+
cmd_integrity() {
|
|
637
|
+
if [ ! -f "$FEATURES" ] || [ ! -f "$QUEUE" ]; then
|
|
638
|
+
echo "[integrity] feature-list 또는 feature-queue 없음"
|
|
639
|
+
exit 0
|
|
640
|
+
fi
|
|
641
|
+
local orphans
|
|
642
|
+
orphans=$(jq -r --slurpfile q "$QUEUE" '
|
|
643
|
+
([.features[].id]) as $fl |
|
|
644
|
+
(($q[0].queue.ready // []) +
|
|
645
|
+
($q[0].queue.passed // []) +
|
|
646
|
+
(($q[0].queue.failed // []) | if type == "object" then keys else . end) +
|
|
647
|
+
(($q[0].queue.in_progress // {}) | keys) +
|
|
648
|
+
(($q[0].queue.blocked // {}) | keys)) as $qids |
|
|
649
|
+
($qids - $fl) | unique | .[]
|
|
650
|
+
' "$FEATURES" 2>/dev/null)
|
|
651
|
+
|
|
652
|
+
if [ -z "$orphans" ]; then
|
|
653
|
+
echo "[integrity] OK — feature-list ↔ queue 동기화됨"
|
|
654
|
+
exit 0
|
|
655
|
+
fi
|
|
656
|
+
echo "[integrity] ⚠ feature-list 에 없는 queue-only feature 발견:"
|
|
657
|
+
echo "$orphans" | sed 's/^/ - /'
|
|
658
|
+
echo "[integrity] 복구 방법:"
|
|
659
|
+
echo " 1) Planner 가 해당 feature 를 feature-list.json 에 sprint 지정해서 추가하거나"
|
|
660
|
+
echo " 2) bash scripts/harness-queue-manager.sh enqueue <feature.json> 로 canonical path 사용"
|
|
661
|
+
exit 1
|
|
662
|
+
}
|
|
663
|
+
|
|
544
664
|
cmd_hold_status() {
|
|
545
665
|
if [ ! -f "$CHECKPOINT" ]; then
|
|
546
666
|
echo '{"held":false}'
|
|
@@ -565,8 +685,10 @@ case "$CMD" in
|
|
|
565
685
|
hold) cmd_hold "$@" ;;
|
|
566
686
|
resume-probe) cmd_resume_probe ;;
|
|
567
687
|
hold-status) cmd_hold_status ;;
|
|
688
|
+
enqueue) cmd_enqueue "$@" ;;
|
|
689
|
+
integrity) cmd_integrity ;;
|
|
568
690
|
*)
|
|
569
|
-
echo "Usage: harness-queue-manager.sh <init|dequeue|auto-dispatch|idle-slots|pass|fail|requeue|recover|next-sprint|update_phase|status|hold|resume-probe|hold-status> [args]"
|
|
691
|
+
echo "Usage: harness-queue-manager.sh <init|dequeue|auto-dispatch|idle-slots|pass|fail|requeue|recover|next-sprint|update_phase|status|hold|resume-probe|hold-status|enqueue|integrity> [args]"
|
|
570
692
|
exit 1
|
|
571
693
|
;;
|
|
572
694
|
esac
|