42 KiB
C-1 구현 계획서 Rev.2 — Kanban 잔재 문서 전면 제거
- Job:
17ffc912/ Role: Planner (Rev.1 = Job75847d1d) - 대상: IMPROVEMENTS.md C-1 — "Kanban 문서 29회 언급 vs 실제 구현 0건"
- 기준 커밋:
924e77e(clean tree) - 반영: Creator
agyChallenge Report (Joba5f24e6f) — 맹점 2건 - 검증 상태: Rev.1 전량 + Rev.2 신규 게이트를
git archive HEAD클론에 실제 적용·측정 완료. 수치는 전부 실측값이다.
0. Rev.1 → Rev.2 변경 요지
| 이의 | 판정 | 근거 | 계획 반영 |
|---|---|---|---|
맹점 1 — status 의 prereq_skills 에 monitor 가 있는 것은 논리 모순이므로 제거 |
기각 (REJECTED) | status.sh:12 가 multi-agent-mux-monitor/scripts/reconcile.sh 를 하드 참조한다. 의존은 실재한다. 또한 prereq_skills 의 저장소 관례는 "선행 실행"이 아니라 "선행 가용성"이며, 이의제기의 전제는 파일 자신의 용례 2건에 의해 반증된다. |
변경 없음. 대신 RK-G 로 위험 기록 + 별도 개선 항목 제안(§9) |
맹점 2 — G-C 정규식이 --goal-max-turns 를 놓친다 |
인용 (ACCEPTED, 확대 적용) | 사실이다. 다만 실측 결과 구멍은 2개가 아니라 10개다. 제안된 --goal 추가는 10개 중 2개만 막는다. |
G-C 전면 재설계 — 열거를 파생(derivation) 으로 교체. red/green 61건 → 0건 실측 |
| (신규) | 추가 | 맹점 1이 제기한 "디스패처가 prereq 메타데이터로 오작동" 우려의 검증 가능한 형태 | G-G — prereq_skills 그래프 순환 부재 게이트 |
총평: 맹점 2의 진단은 정확했고 처방은 부족했다. 맹점 1은 진단 자체가 사실과 어긋난다. 상세는 §7.
1. 요약
C-1 은 "단어 29개를 지운다"가 아니라 세 종류의 서로 다른 문제다. 셋을 같은 방법으로 처리하면 실패한다.
| 분류 | 건수 | 성격 | 처리 |
|---|---|---|---|
| N. 순수 노이즈 | 12 | 지워도 의미 손실 0 (수식어, 태그, 괄호) | 단어 삭제 |
| D. 끊어진 스킬 참조 | 3 | 이 저장소에 존재하지 않는 스킬을 가리킴 (kanban-worker, kanban-orchestrator) |
실존 스킬로 교체 |
| S. 구조적 의존 | 16 | 문서의 실행 메커니즘 자체가 Kanban (hermes kanban create, kanban_comment/_block/_heartbeat, goal_mode) |
섹션 재작성 |
핵심 판단: S 분류는 단어를 지우면 문서가 "무엇을 실행하라는 것인지" 알 수 없게 된다. 대체 메커니즘이 반드시 필요하고, 이미 구현되어 있다 — reconcile.sh --subscribe 가 MQTT push + 브로커 다운 시 폴링 폴백을 갖춘 자립형 상주 루프다. 즉 Kanban 은 처음부터 불필요한 껍데기였고, 제거는 문서를 실제 구현에 일치시키는 작업이다.
부가 성과: 문서 정합성 결함 2건 동시 해소(§6). 하나는 존재하지 않는 상태 파일 주장, 하나는 --subscribe 폴백 stdout 동작 오기.
2. 현황 측정 (근거)
$ grep -ril kanban . | grep -v '^./.git/' → 파일 6개
22 .agents/skills/multi-agent-mux-monitor/SKILL.md
5 .agents/skills/multi-agent-mux-status/SKILL.md
2 .agents/skills/multi-agent-mux-create/SKILL.md
1 README.md
1 README.ko.md
2 IMPROVEMENTS.md ← C-1 추적 항목 자신
- SKILL.md 합계 = 22 + 5 + 2 = 29. C-1 의 "29회" 와 정확히 일치 → 추적 항목의 전제는 유효하다.
- 코드 파일 참조 0건:
grep -ril kanban에서.md를 빼면 아무것도 남지 않는다.hermes kanban서브커맨드를 쓰는 코드도 0건. reconcile.sh내kanban관련 코드 = 0건. 문서가 서술한kanban_comment호출은 애초에 어디에도 없다.
→ 순수 문서 작업이며 런타임 동작 변경 위험은 0이다.
주의 —
hermes자체는 실존 도구다.create_session.sh:102가hermes status를 호출하고,create/SKILL.md:234는hermes profile alias사용 금지를 정당하게 경고한다. 제거 대상은hermes kanban서브커맨드 표면이지hermes전체가 아니다. Rev.2 검토 중 "스킬 문서에hermesCLI 호출 금지" 게이트를 후보로 올렸다가 이 실측으로 폐기했다 — 정당한 2건을 오탐했을 것이다.
3. 범위 확정 — 리뷰어의 "0건"은 어디까지인가
성공 조건은 "reviewers verifying 0 remaining references" 다. 리뷰어가 grep -ri kanban . 를 그냥 실행하면 IMPROVEMENTS.md 가 걸린다. 그런데 C-1 항목은 제거 사실을 기록하는 추적 문서이므로 이름을 언급하지 않고는 쓸 수 없다. 방치하면 리뷰가 무한히 실패한다.
해소 규칙:
- G-A (제품 표면) =
.agents/skills/+README.md+README.ko.md→ 반드시 0건. 실질 합격 기준. - G-B (저장소 전체 − 추적/보고 문서) =
IMPROVEMENTS.md와.mam/(잡 브리프·리포트가 구 문안을 인용) 제외 → 0건. - IMPROVEMENTS.md 의 C-1 은 삭제가 아니라 §5 완료된 과제로 이관(파일 기존 관례). 잔존 2회는 "완료 기록"으로 의도된 것이며 G-A/G-B 어디에도 해당하지 않는다.
리뷰어가 스스로 grep 범위를 즉흥 결정하지 않게 하는 것이 이 절의 목적이다.
4. 변경 명세
F1 — .agents/skills/multi-agent-mux-monitor/SKILL.md (22건, 전면 재작성)
이 파일만 파일 전체 교체를 지시한다. 22개 참조가 10개 섹션에 흩어져 있고 §Dispatching / §Worker body template 두 섹션은 통째로 Kanban 스펙이라, 부분 치환 지시로는 Creator 마다 결과가 갈린다. 최종 전문은 부록 A(검증 완료본, 218줄).
변경 인벤토리 — 22건 전량의 처분:
| 위치 | 원문 | 처분 | 분류 |
|---|---|---|---|
L3 description |
"long-lived Kanban worker", "dispatched as a Kanban goal_mode task (--goal)" | reconcile.sh --subscribe 상주 루프로 재서술 |
S |
L8 environments |
[kanban, terminal, herdr] |
[terminal, herdr] — 타 스킬 6종 전부와 동일해짐 |
N |
L11 tags |
..., monitor, kanban, observation, ... |
kanban 제거 |
N |
L12 related_skills |
..., kanban-orchestrator |
..., multi-agent-mux-status |
D |
L13 prereq_skills |
[kanban-worker, multi-agent-mux-create] |
[multi-agent-mux-create] |
D |
| L16 H1 | "— Live Reconciliation via Kanban Worker" | "— Live Reconciliation" | N |
| L23 | "Dispatch a Kanban worker (in goal_mode) that:" |
"Run a long-lived reconciler (reconcile.sh --subscribe) that:" |
S |
| L25 | "Every ~30s polls" | 실제 동작으로 정정: MQTT 이벤트 반응 + 브로커 다운 시 RECONCILE_POLL_INTERVAL(기본 15s) 폴백 |
S |
| L37 | 4. "Writes a Kanban kanban_comment on every drift event" |
4. --emit-diff 시 stdout JSON 방출 (+폴백 경로 단서, §6-b) |
S |
| L39 | 6. "Goal loop: judge … worker blocks with reason=stop-requested" |
5. 실제 종료 조건: --timeout / --idle-timeout / 인터럽트 |
S |
| L52 | "You don't have a Kanban dispatcher running" | "You only need a point-in-time answer — use multi-agent-mux-status" |
S |
| L54-85 | §Dispatching the monitor — hermes kanban create 30줄 블록 |
§Running the monitor — 실제 실행 2줄로 교체. body 내 herdr 네이티브 명령 경고문은 보존 | S ×4 |
| L93 | "JSON diff … that the worker turns into a kanban_comment" |
"for the caller to consume" | S |
| L110 | 플래그 문단 말미 "…turns the emitted JSON drifts[] into kanban_comment calls itself." |
"…the emitted JSON drifts[] is the caller's to consume." — --comment-card 언급도 제거 |
S |
| L167 | 함정 "Don't run the monitor without --goal" |
실존 함정으로 교체: --once 는 1회 후 종료 / --idle-timeout 기본 3600s 함정 |
S |
| L169 | 함정 "kanban_comment rate limits … .cache/…/<ws>.state 를 추적한다" |
중복 병합 책임을 caller 로 명시. 거짓 주장 제거 → §6-a | S |
| L171 | "comment about it but don't delete" | "report it but don't delete" | N |
| L174-209 | §Worker body template — kanban_comment/_heartbeat/_block/$HERMES_KANBAN_TASK |
§Supervising-agent runbook 으로 전환. Hard rules 4개 전량 보존 | S ×4 |
| L119-163 | drift A~D 예시의 "comment:" | "report:" | N ×4 |
| L236-237 | §Related skills — kanban-worker, kanban-orchestrator |
multi-agent-mux-status, multi-agent-mux-delegate-job |
D |
의도적으로 남기는 것: metadata.hermes 블록 자체(Kanban 아님, 공통 스키마), hermes 태그 체계, §Security --subscribe 전문, §Status Enum, drift A~D 본문.
F2 — .agents/skills/multi-agent-mux-status/SKILL.md (5건, 6줄 치환)
각 치환은 원문에 정확히 1회만 등장함을 스크립트로 강제 확인한다(0회/2회면 즉시 중단).
| 줄 | before → after |
|---|---|
| L3 | …and any drift. No Kanban, no mutation. Reuses… → …and any drift. No mutation. Reuses… |
| L3 | without spinning up a Kanban monitor worker. → without spinning up the monitor loop. |
| L24 | **No mutation. No Kanban. No polling loop.** → **No mutation. No polling loop.** |
| L108 | dispatch \multi-agent-mux-monitor` (Kanban worker) or run→run `multi-agent-mux-monitor` or` |
| L118 | - Quick sanity check before dispatching a long Kanban task → …before dispatching a long-running job |
| L122 | - Continuous live tracking → \multi-agent-mux-monitor` (Kanban worker)→…→ `multi-agent-mux-monitor`` |
frontmatter 는 손대지 않는다. L12/L13 의
related_skills/prereq_skills는 Rev.1 과 동일하게 원형 유지한다. 근거는 §7-A.
F3 — .agents/skills/multi-agent-mux-create/SKILL.md (2건)
| 줄 | before → after |
|---|---|
| L51 | If any check fails → \kanban_block(reason="...")` (worker path) or report to user (interactive path).→If any check fails → abort with a non-zero exit and report the reason (automated path) or report to user (interactive path).` |
| L65 | (e.g., cmux, Kanban workers, manual herdr sessions) → (e.g., cmux, background workers, manual herdr sessions) |
L51 은 단순 삭제하면 "실패 시 무엇을 하라"가 사라지므로 동등한 비-Kanban 지시로 치환해야 한다.
F4 — README.md (1건) / README.ko.md (1건)
| 파일 | before → after |
|---|---|
| README.md L45 | A long-running Kanban reconcile worker that dynamically monitors → A long-running reconcile worker that dynamically monitors |
| README.ko.md L27 | 백그라운드에서 Kanban Reconcile 프로세스로 실행되어 → 백그라운드 Reconcile 프로세스로 실행되어 |
브리프 문구는 "SKILL.md files" 지만 §3 의 이유로 README 2건 필수 포함.
F5 — IMPROVEMENTS.md (추적 항목 이관)
- §4 에서 C-1 블록(L81-82) 삭제
- §4 헤더
(Legacy Remnants — 5건)→(Legacy Remnants — 4건) - §5 헤더
(Completed Tasks — 3건)→(Completed Tasks — 4건) - §5 에 파일 기존 형식으로 추가:
### **C-1: Kanban 문서 29회 언급 vs 실제 구현 0건** — ✅ 완료
- SKILL.md 3종(monitor 22 / status 5 / create 2)과 README 2종의 Kanban 서술을 전면 제거했습니다.
- `multi-agent-mux-monitor` 의 실행 메커니즘 서술을 실제 구현인 `reconcile.sh --subscribe`
(MQTT push + 브로커 다운 시 폴링 폴백) 기준으로 재작성했습니다.
- 존재하지 않는 스킬 참조 2건(`kanban-worker`, `kanban-orchestrator`)을 실존 스킬로 교체했습니다.
- `hermes kanban create` CLI 플래그 잔재 10종(`--goal-max-turns`, `--assignee`, `--comment-card` 등)을
파생형 검증 게이트(G-C)로 차단했습니다.
- 제품 표면(`.agents/skills/`, `README*.md`) Kanban 참조 **0건** 확인.
F6 — 반영하지 않는 것
.mam/하위 잡 브리프·리포트: 과거 잡의 기록이며 수정 시 감사 추적 훼손. G-B 가 제외하는 이유.status/SKILL.mdfrontmatterprereq_skills: §7-A..cache코드: C-2 소관(§6-a).
5. 적용 순서
F1 → F2 → F3 → F4 → G 게이트 전량 실행 → F5(IMPROVEMENTS 이관) → 재실행.
F5 를 마지막에 두는 이유: 완료 기록은 게이트 통과 후에 쓰여야 한다. 먼저 쓰면 미완료를 완료로 기록한 순간이 커밋 이력에 남는다.
6. 검증 게이트
전부 클론에 적용해 실측 완료. 괄호 안이 측정값이다.
G-A — 제품 표면 0건 (실질 합격 기준) (측정: 0 — CLEAN)
grep -ric kanban .agents/skills README.md README.ko.md | grep -v ':0$' || echo "0 references — CLEAN"
G-B — 저장소 전체 0건 (추적/보고 문서 제외) (측정: 0 — CLEAN)
grep -ril kanban . | grep -v '^\./\.git/' | grep -v IMPROVEMENTS.md | grep -v '^\./\.mam/' \
|| echo "0 files — CLEAN"
G-C — Kanban CLI/API 표면 잔재 0건 ⚠️ Rev.2 에서 전면 재설계
(측정: HEAD 61건 FAIL → 적용 후 0건 PASS)
Rev.1 의 결함: 패턴을 손으로 열거했다(KANBAN|kanban_|goal_mode|kanban-worker|kanban-orchestrator|HERMES_KANBAN). 손 열거는 반드시 빠뜨린다. agy 가 --goal-max-turns 로 이를 정확히 지적했다.
그러나 구멍은 2개가 아니라 10개였다. F1 이 삭제하는 텍스트에서 토큰을 기계적으로 추출해 각 패턴의 포착 여부를 실측한 결과:
| 삭제되는 토큰 | Rev.1 패턴 | agy 제안 패턴 |
Rev.2 파생 게이트 |
|---|---|---|---|
kanban_comment / kanban_block / kanban_heartbeat |
CATCH | CATCH | CATCH |
HERMES_KANBAN_TASK / goal_mode |
CATCH | CATCH | CATCH |
--goal / --goal-max-turns |
MISS | CATCH | CATCH |
--assignee |
MISS | MISS | CATCH |
--comment-card |
MISS | MISS | CATCH |
--max-runtime |
MISS | MISS | CATCH |
--max-retries |
MISS | MISS | CATCH |
--title / --branch / --body / --skill |
MISS | MISS | CATCH |
→ agy 의 처방은 10개 중 2개만 막는다. 진단은 옳았으나 처방이 부족하다.
Rev.2 의 해법: 열거를 파생으로 바꾼다. 토큰 목록을 기억이 아니라 F1 이 실제로 삭제하는 텍스트에서 뽑는다 — 구성상 완전하다.
python3 .mam/gates/gate_gc.py # 전문은 부록 C. 실패 시 exit 1
HEAD: G-C FAIL — 61 Kanban-surface hit(s)
적용 후: G-C PASS — 0 Kanban CLI/API surface tokens
오탐 위험 실측: 후보 플래그 10종이 monitor/SKILL.md 밖 제품 표면에 등장하는 횟수 = 전부 0. 오탐 없음.
의도적 제외 1건: AGENT_SESSIONS_STATE_DIR 도 F1 이 삭제하는 토큰이지만 게이트에 넣지 않는다. reconcile.sh:23 에 실존하는 환경변수이며, 삭제 사유는 Kanban 이 아니라 거짓 서술(§6-a)이다. 훗날 올바르게 재문서화될 수 있으므로 금지어로 만들면 안 된다.
G-D — 전 SKILL.md frontmatter YAML 파싱 유지 (측정: 7/7 OK)
python3 - <<'PY'
import glob, yaml
for f in sorted(glob.glob('.agents/skills/*/SKILL.md')):
t = open(f, encoding='utf-8').read()
if not t.startswith('---'): continue
d = yaml.safe_load(t.split('---',2)[1])
print(f" OK {d['name']:36s} env={d.get('environments')}")
PY
G-E — 끊어진 스킬 참조 0건 (red→green) (HEAD 2건 → 적용 후 0건)
단어를 지운 게 아니라 실존하는 것으로 바꿨음을 증명한다.
python3 - <<'PY'
import glob, os, yaml
local = {os.path.basename(os.path.dirname(f)) for f in glob.glob('.agents/skills/*/SKILL.md')}
EXTERNAL_OK = {'claude-code'}
bad = []
for f in sorted(glob.glob('.agents/skills/*/SKILL.md')):
d = yaml.safe_load(open(f, encoding='utf-8').read().split('---',2)[1]) or {}
h = (d.get('metadata') or {}).get('hermes') or {}
for key in ('related_skills','prereq_skills'):
for s in h.get(key) or []:
if s not in local and s not in EXTERNAL_OK:
bad.append((os.path.basename(os.path.dirname(f)), key, s))
print("DANGLING:", bad or "none — CLEAN")
PY
HEAD: [('multi-agent-mux-monitor','related_skills','kanban-orchestrator'),
('multi-agent-mux-monitor','prereq_skills','kanban-worker')]
적용 후: none — CLEAN
G-G — prereq_skills 그래프 순환 부재 🆕 Rev.2 신규
(측정: HEAD DAG → 적용 후 DAG)
맹점 1이 제기한 "자율 디스패처가 prereq 메타데이터로 오작동한다"는 우려의 검증 가능한 형태다. 디스패처를 실제로 무한루프에 빠뜨리는 것은 논쟁의 여지가 있는 의미론이 아니라 순환이다. F1 이 monitor.related_skills 에 status 를 넣으므로(status.related_skills 에는 이미 monitor 존재) 상호 참조가 생기는데, related_skills 의 상호 참조는 무해하고 create↔resume 에 이미 존재한다. 위험한 것은 prereq_skills 순환뿐이며, 이 게이트가 그것만 정확히 겨냥한다.
python3 - <<'PY'
import glob, yaml
g = {}
for f in glob.glob('.agents/skills/*/SKILL.md'):
d = yaml.safe_load(open(f, encoding='utf-8').read().split('---',2)[1]) or {}
g[d['name']] = list(((d.get('metadata') or {}).get('hermes') or {}).get('prereq_skills') or [])
def cyc(n, seen):
if n in seen: return seen[seen.index(n):] + [n]
for m in g.get(n, []):
if m in g:
r = cyc(m, seen + [n])
if r: return r
return None
found = [c for n in g if (c := cyc(n, []))]
print("CYCLES:", found or "none — DAG")
PY
적용 후 실측 그래프:
create -> ['claude-code'] monitor -> ['multi-agent-mux-create']
loop -> ['multi-agent-mux-create'] resume -> ['multi-agent-mux-create']
status -> ['multi-agent-mux-create', 'multi-agent-mux-monitor']
stop -> ['multi-agent-mux-create', 'multi-agent-mux-resume']
delegate-job -> []
CYCLES: none — DAG
G-F — 회귀 테스트: 불필요 (명시적 판단)
tests/ 전량에 SKILL.md·README 를 읽는 코드가 0건(grep -rln "SKILL.md\|README" tests/ → 없음), CI 문서 린트 없음(.github/workflows/ 부재), 변경 파일에 스크립트 0개. 8분짜리 전체 스위트는 이 변경에 대해 아무것도 증명하지 못한다. 형식적 완결성을 위해 돌리는 것을 막지 않되 계획은 요구하지 않는다. 실제 위험은 G-A~G-G 가 덮는다.
측정된 변경 규모 (diff -ru base clone, 총 328줄):
-74 +53 .agents/skills/multi-agent-mux-monitor/SKILL.md (237줄 → 218줄)
-5 +5 .agents/skills/multi-agent-mux-status/SKILL.md
-2 +2 .agents/skills/multi-agent-mux-create/SKILL.md
-1 +1 README.md
-1 +1 README.ko.md
7. 이의제기 심층 판정
7-A. 맹점 1 — status.prereq_skills 에서 monitor 제거 → 기각
주장: status 는 단발성 read-only 스냅샷이므로 monitor 를 선행 필수로 두는 것은 논리적 모순이다.
반증 1 — 의존은 실재한다.
.agents/skills/multi-agent-mux-status/scripts/status.sh:12
RECONCILE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/multi-agent-mux-monitor/scripts/reconcile.sh"
status.sh 는 monitor 스킬 디렉터리의 스크립트를 경로로 하드 참조한다. status/SKILL.md:46 도 "Calls reconcile.sh --once --emit-diff --dry-run" 이라 명시한다. monitor 스킬을 제거하면 status 는 즉시 깨진다. 이는 저장소 전체에서 prereq_skills 항목이 파일시스템 수준에서 문자 그대로 참인 유일한 사례다. 제거하면 메타데이터가 덜 정확해진다.
반증 2 — "선행 실행" 해석은 파일 자신의 용례가 부정한다. 실측된 6개 전량:
create -> ['claude-code'] ← 스킬로 "실행"하는 대상이 아님. 설치 여부다.
stop -> ['multi-agent-mux-create','...-resume'] ← stop 전에 resume 을 실행할 필요는 없다.
두 건이 각각 독립적으로 "선행 실행" 해석을 반증한다. 저장소 관례는 가용성(availability) 이다. 그 해석에서 status -> monitor 는 모순이 아니라 정확한 서술이다.
반증 3 — 소비자가 없다. prereq_skills / related_skills / environments 를 읽는 코드는 저장소 전체에 0건(.py/.sh 전수 검색). 주장된 "디스패처 오작동"은 이 저장소 안에서는 재현 불가능한 가정이다. 외부 디스패처를 가정하더라도 반증 1·2 가 우선한다.
그럼에도 남는 실재 문제: prereq_skills 의 의미론이 어디에도 정의되어 있지 않다. agy 가 그럴듯하게 오독했다는 사실 자체가 모호성의 증거다. 다만 올바른 해소는 참인 의존을 지우는 것이 아니라 의미론을 문서화하는 것이며, 이는 C-1(Kanban 제거)과 무관한 별개 과제다 → §9 로 분리 제안.
7-B. 맹점 2 — G-C 정규식 보강 → 인용하되 확대
지적은 정확하다. Rev.1 G-C 는 --goal-max-turns 를 놓쳤다. 다만 §6 G-C 표가 보이듯 놓친 것은 10개이고 제안된 --goal 추가는 그중 2개만 막는다. 나머지 8개(--assignee, --comment-card, --max-runtime, --max-retries, --title, --branch, --body, --skill)는 agy 의 패턴으로도 통과한다.
근본 원인은 특정 단어 누락이 아니라 "기억으로 열거한 목록"이라는 방법론이다. 그래서 Rev.2 는 단어를 더 넣는 대신 목록을 삭제 대상 텍스트에서 파생시킨다. 이 방법에서는 정의상 누락이 발생하지 않는다.
이 지적이 없었다면 Rev.1 은 8개의 추가 구멍을 가진 채 통과했을 것이다. 부분 편집(전체 교체가 아닌)으로 F1 을 수행한 Creator에게는 실제로 발생할 수 있는 실패였다.
8. 함께 해소되는 문서 정합성 결함 2건
(a) 존재하지 않는 상태 파일 — C-2 와 교차
원문 L169 는 "The script tracks a state file at .cache/multi-agent-mux-monitor/<workspace>.state" 라 단언한다. 실측:
reconcile.sh:23 STATE_DIR="${AGENT_SESSIONS_STATE_DIR:-…/.cache/multi-agent-mux-monitor}"
reconcile.sh:313 mkdir -p "$STATE_DIR"
→ 읽기 0회, 쓰기 0회
mkdir 만 하고 아무것도 저장하지 않는다(IMPROVEMENTS C-2 가 코드 측면에서 동일 사실 추적 중). 대체 문장은 중복 병합 책임을 caller 에게 명시하고 상태 파일 주장은 싣지 않는다.
이 계획은
.cache코드를 건드리지 않는다. 그건 C-2 소관이다. 문서가 거짓을 말하지 않게만 한다.
(b) --subscribe 폴백 경로의 stdout 동작
Rev.1 초안에서 4번 항목을 "모든 drift 를 stdout JSON 으로 방출"이라 썼다가 스크립트 확인 중 정정했다:
reconcile.sh:303 bash "$_self" --once --emit-diff >/dev/null 2>&1 || true
브로커 다운 폴백 루프는 stdout 을 버린다. --subscribe 상태에서 drift JSON 을 소비할 수 없다. 최종 문안은 이 단서를 명시한다 — 원문에 없던 정보이며, 이를 믿고 파이프를 붙이려는 사용자를 구한다.
9. 후속 제안 (C-1 범위 밖, 별도 항목)
제안: prereq_skills / related_skills 의미론 정의 — §7-A 가 드러낸 실재 모호성이다.
- 현상: 두 필드의 의미가 어디에도 정의되지 않아 "선행 실행"과 "선행 가용성"으로 갈리게 읽힌다. 실제로 Creator 가 후자를 전자로 오독해 참인 의존을 삭제하자고 제안했다.
- 소비 코드 0건이므로 런타임 영향은 없으나, 외부 디스패처 연동 시 실제 오작동 원인이 된다.
- 제안:
MULTI_AGENT_RULES.md또는 스킬 작성 가이드에 1문단으로 정의하고, G-E·G-G 를 상시 린트로 승격. - C-1 에 병합하지 않는 이유: Kanban 잔재 제거와 무관하며, 병합하면 "0건 검증"이라는 단일 합격 기준이 흐려진다.
10. 리스크
| ID | 리스크 | 평가 / 완화 |
|---|---|---|
| RK-A | 런타임 회귀 | 없음. 변경 5파일 전부 .md. 코드 참조 0건 실측. |
| RK-B | environments: [kanban,…] 제거가 외부 스킬 레지스트리 로딩을 깬다 |
저장소 내 environments 소비 코드 0건. 나머지 6개 스킬이 이미 [terminal, herdr] → 다수 관례에 합류. |
| RK-C | §Dispatching 삭제로 "어떻게 띄우나"를 잃음 | 삭제가 아니라 교체. hermes 미설치 상태에서 원문 명령은 애초에 실행 불가였고, --subscribe 가 실제 동작하는 유일한 방법이다. |
| RK-D | 리뷰어가 IMPROVEMENTS.md 잔존 2건으로 FAIL 판정 | §3 에서 범위 사전 정의 + G-A/G-B 명령 제공. |
| RK-E | Worker body template 삭제로 Hard rules 상실 | §Supervising-agent runbook 으로 전환, Hard rules 4개 전량 보존. |
| RK-F | 향후 Kanban 문구 복귀 | G-C(파생형)를 리뷰 체크리스트 상시 항목으로. 자동 린트 승격은 §9. |
| RK-G 🆕 | 후속 리뷰어가 맹점 1을 재차 제기해 status.prereq_skills 를 임의 수정 |
§7-A 를 계획에 명시적으로 남겨 판정 근거(status.sh:12)를 보존. 삭제 시 G-E 는 통과하지만 문서가 거짓이 된다 — 게이트로는 잡히지 않는 유형이므로 서술로 방어한다. |
| RK-H 🆕 | G-C 의 --title/--body/--branch/--skill 이 훗날 정당하게 등장해 오탐 |
현재 제품 표면 등장 0건 실측. 오탐 시 해당 토큰만 근거와 함께 제외하고, 파생 방식 자체는 유지할 것. |
11. 역할 경계
본 문서는 설계 산출물이며 저장소 코드·문서는 한 줄도 수정하지 않았다. 모든 프로토타이핑은 세션 스크래치패드 내 git archive HEAD 클론에서만 수행했고, 작업 트리는 924e77e 기준 clean 을 유지한다.
MULTI_AGENT_RULES.md §1 에 따라 구현은 Creator, 커밋은 GM 소관이다. 차단 항목 없음.
부록 A — multi-agent-mux-monitor/SKILL.md 최종 전문 (F1 적용본, 218줄)
---
name: multi-agent-mux-monitor
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
version: 1.0.0
author: godopu
license: MIT
platforms: [linux, macos]
environments: [terminal, herdr]
metadata:
hermes:
tags: [agent, herdr, claude, antigravity, agy, monitor, observation, reconciliation]
related_skills: [multi-agent-mux-create, multi-agent-mux-resume, multi-agent-mux-stop, multi-agent-mux-status]
prereq_skills: [multi-agent-mux-create]
---
# Agent Sessions Monitor — Live Reconciliation
> **Companion skills**: `multi-agent-mux-create` / `multi-agent-mux-resume` / `multi-agent-mux-stop` (mutators); this skill is the **observer**.
> **Single source of truth**: `./.mam/agent-sessions.yaml`.
## What this skill does
Run a **long-lived reconciler** (`reconcile.sh --subscribe`) that:
1. Reacts to delegated-job events on the MQTT broker, and — whenever the broker is
unreachable — falls back to polling every `RECONCILE_POLL_INTERVAL` (default 15s)
the actual state of:
- `herdr agent list` (which sessions are alive)
- `herdr agent get <session>` (pane cmd, cwd)
- `~/.claude/projects/<workspace-key>/*.jsonl` mtime + first-line sessionId
- `~/.gemini/antigravity-cli/cache/last_conversations.json` (agy workspace → conversation mapping)
- `~/.gemini/antigravity-cli/conversations/<uuid>.db` mtime (agy)
2. Compares the live state to `agent-sessions.yaml`
3. Detects 4 classes of drift:
- **yaml-only terminated/archived/stopped**: herdr dead, YAML says `terminated`, `archived`, or `stopped` → OK, left untouched (deliberate end states)
- **yaml-only running, herdr dead**: YAML says `running`, herdr is gone → mark `terminated` with timestamp
- **herdr-only running, not in YAML**: herdr session exists with `<workspace>-creator-*` naming but YAML doesn't know about it → register as a new entry
- **stale UUID**: YAML has a UUID, but the on-disk artifact is gone → report it
4. Emits a JSON drift record on stdout for every drift event when run with `--emit-diff`
(note: the `--subscribe` broker-down fallback runs each pass for its YAML side-effects
and discards the JSON — capture drift output with an explicit `--once --emit-diff`)
5. Keeps running until one of its exit conditions fires: `--timeout` (wall-clock),
`--idle-timeout` (no message received), or an interrupt from the operator.
## When to use
- You have multiple workspaces with herdr agent sessions and want a single source of truth
- You suspect YAML drift after a host reboot / crash
- You want a notification when a session id was just created (so you can record it before next restart)
- You're running multi-day work and want to know "what's actually running right now"
## When NOT to use
- One-off interactive session — just check `herdr agent list` and read the YAML
- A single, short session — overhead > benefit
- You only need a point-in-time answer — use `multi-agent-mux-status` instead
## Running the monitor
```bash
# Persistent monitor: runs until interrupted; polls if the broker is unreachable.
bash .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe --idle-timeout 0
# Bounded run: exits after 5 min with no message, or 1 h wall-clock, whichever comes first.
bash .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --subscribe --idle-timeout 300 --timeout 3600
```
Run it under whatever supervisor you already use (a dedicated herdr pane, `nohup`,
or a background job). Nothing else needs to be running for the monitor to work —
it reconciles YAML ↔ herdr ↔ disk on its own.
The herdr commands the script issues (`herdr agent list`, `herdr agent get <session>`)
are real native herdr commands — do not substitute tmux-era names like `herdr ls` /
`herdr list-panes` outside a shell that has sourced `.agents/skills/lib.sh`.
## Helper script: `reconcile.sh`
This is the whole monitor — there is no separate driver. Each pass:
1. Diffs YAML ↔ herdr ↔ disk artifacts
2. Updates YAML if needed (only when changes are real, not on every poll — avoids spamming)
3. Emits a JSON diff to stdout for the caller to consume
```bash
# Reconcile + auto-update YAML (atomic, flock-guarded). Emits JSON drift to stdout.
bash .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --once --emit-diff
# Read-only: compute drift WITHOUT writing the YAML (use for "what's running?" checks).
bash .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --once --emit-diff --dry-run
```
Flags: `--once` (single pass), `--emit-diff` (print JSON), `--dry-run` (P1-E — no mutation), `--subscribe` (push-based MQTT subscription monitoring). `--subscribe` sub-flags: `--timeout N` (exit after N seconds of wall-clock; `0` = no limit, default), `--idle-timeout N` (exit after N seconds with no message; default `3600`, `0` = never idle-out). On a broker connection failure (connect error **or** non-zero CONNACK), `--subscribe` falls back to a polling loop that re-runs `--once --emit-diff` every `RECONCILE_POLL_INTERVAL` (default 15) seconds until `--timeout`. Terminal-event YAML updates are written through `lib.sh::atomic_dump_yaml` (flock + schema-validate + `.bak`). There are **no** `--workspace` / `--agent` flags; the emitted JSON `drifts[]` is the caller's to consume.
## Drift classes (what the script handles)
### Status Enum
The `status` field MUST be one of the following exact strings: `running`, `stopped`, `terminated`, `archived`.
The `last_visible_status` is a free-form human-readable status string (e.g. verification-cycle states: `unverified`, `pinned`, `resume_verified`, or a failure detail string) and is NOT constrained to this enum.
Any unstructured comments or reasons for the status change should be placed in `last_visible_note` or `termination_mode`.
### A. herdr dead, YAML says running → auto-terminate
```
YAML: status=running, pane.pid=201132, cmd=claude
herdr: no session
→ set status=terminated, terminated_at=<now>, termination_mode=auto-detected
→ report: "lab-landing-page-creator-claude: herdr gone (was pane 201132, cmd claude). Marked terminated."
```
**Skip-set**: the auto-terminate only fires for sessions whose status is `running`.
Rows already in a deliberate end state — `terminated`, `archived`, or **`stopped`**
(set by `multi-agent-mux-stop`) — are
left untouched. This is critical: a `stopped` row keeps its `resumable: true` and
captured `*_session_id_own`, so the monitor must **not** overwrite it with
`terminated ("auto-detected")` when its herdr is (expectedly) gone.
### B. herdr alive, not in YAML → auto-register
```
herdr: session=lab-paper-pdf2md-creator-agy, pid=...,
cmd=agy, cwd=$WORKSPACE_ROOT/paper-pdf2md
YAML: no such session
→ register as new entry: status=running, last_visible_status=running, last_visible_note=auto-registered
→ report: "lab-paper-pdf2md-creator-agy: herdr found but not in YAML. Auto-registered."
```
### C. New session id materializes (claude first message sent)
```
YAML: claude_session_id_own=null (placeholder)
disk: ~/.claude/projects/.../b3a7...c2f.jsonl exists, mtime=now,
first line sessionId=b3a7...c2f
→ update claude_session_id_own=b3a7...c2f
→ report: "lab-landing-page-creator-claude: session id materialized b3a7...c2f"
```
### D. Stale UUID (artifact gone)
```
YAML: agent_identities.claude.session_id=87dc548e-...
disk: ~/.claude/projects/.../87dc548e-...jsonl: missing
→ report it, but DO NOT delete from YAML
(the user may have moved the file or the disk may be temporarily unavailable;
only `--purge-conversation` should remove the id)
```
## Pitfalls
- **Don't expect `--once` to stay alive** — it does a single pass and exits. Use `--subscribe` for continuous monitoring.
- **`--idle-timeout` defaults to 3600s** — a monitor meant to run indefinitely needs `--idle-timeout 0` explicitly, or it will quietly exit after an hour of broker silence.
- **The poll interval is a default** — `RECONCILE_POLL_INTERVAL` (15s) is what the broker-down fallback uses. A workspace with 5+ agent sessions can bump it to reduce noise.
- **Coalesce repeated drifts** — the same drift re-appears on every pass until it is resolved. A caller that acts on `drifts[]` should compare against the previous pass and act only on *new* entries; the script does not deduplicate for you.
- **Don't fight the user's explicit action** — if `multi-agent-mux-stop` is mid-flight and the monitor sees the same session in two states within 5s, prefer the user's most recent action. The monitor should not auto-revert a fresh `terminated` to `running` because of a stale `herdr has-session` check.
- **The monitor should never modify the conversation artifacts** (jsonl, db) — only the YAML. If you see a stale UUID, report it but don't delete the file.
- **TUI capture-pane is expensive** — only capture when you need to update `last_visible_status`, not every poll.
## Supervising-agent runbook
If an agent drives the monitor rather than an operator watching it directly, this is
the behavior spec:
```markdown
# agent-sessions monitor
## Loop
1. Read agent-sessions.yaml
2. Bash: `bash .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --once --emit-diff`
3. Parse the JSON diff from stdout
4. If `drifts` is non-empty, report each *new* drift to the operator
5. Bash: `sleep 30`, then repeat
## Stop condition
Stop when the operator says to stop, or when the surrounding job's timeout fires.
## Drift responses
- A. herdr dead + YAML running: auto-terminate YAML, report
- B. herdr alive not in YAML: auto-register, report
- C. New session id from *.jsonl: update YAML, report
- D. Stale UUID: report only, no YAML change
## Hard rules
- Do NOT modify conversation artifacts (jsonl, db, brain/)
- Do NOT spawn/delete herdr sessions — that's the create/delete skills' job
- Do NOT call multi-agent-mux-create or multi-agent-mux-stop — only the user initiates those
- Do NOT call `git commit` / `git push`
```
## Security: --subscribe on Public Brokers
When using `--subscribe` with the default PoC public broker
(`broker.hivemq.com:1883`), be aware that:
1. **Wildcard subscription** means anyone can publish events to your job topics.
2. **Auto-kill on terminal events** means a spoofed `completed` or `error`
event from a third party can terminate your agent session.
3. **Mitigation**: Use `--subscribe` only on private TLS-enabled brokers
(production mode). For PoC, prefer polling-based monitor (`--once` or
no `--subscribe`) which reads YAML/herdr state directly without MQTT.
4. **HMAC verification**: Events are now verified via `verify_hmac()` in
`mqtt_common.py` (see FW-05). Ensure `auth_token` is set for each job
to enable signature validation — unauthenticated events will be dropped.
## Verification (one-shot)
```bash
# Run reconcile once and inspect output
bash .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh --emit-diff --once \
| python3 -m json.tool
```
## Related skills
- `multi-agent-mux-status` — read-only snapshot when you don't need a running loop
- `multi-agent-mux-delegate-job` — the MQTT job channel whose events `--subscribe` listens to
부록 B — F2~F4 자동 적용 스크립트
각 치환의 등장 횟수가 정확히 1인지 검사하고 아니면 즉시 중단한다. replace_all 사고를 구조적으로 막는다.
import io, sys
def sub(path, pairs):
s = io.open(path, encoding='utf-8').read()
for old, new in pairs:
n = s.count(old)
if n != 1:
print(f"!! {path}: expected 1 occurrence, got {n} for: {old[:70]!r}")
sys.exit(1)
s = s.replace(old, new)
io.open(path, 'w', encoding='utf-8').write(s)
print(f"ok {path} ({len(pairs)} edits)")
sub('.agents/skills/multi-agent-mux-status/SKILL.md', [
("resume UUID on disk, and any drift. No Kanban, no mutation. Reuses",
"resume UUID on disk, and any drift. No mutation. Reuses"),
("without spinning up a Kanban monitor worker.",
"without spinning up the monitor loop."),
("**No mutation. No Kanban. No polling loop.**",
"**No mutation. No polling loop.**"),
("dispatch `multi-agent-mux-monitor` (Kanban worker) or run",
"run `multi-agent-mux-monitor` or"),
("- Quick sanity check before dispatching a long Kanban task",
"- Quick sanity check before dispatching a long-running job"),
("- Continuous live tracking → `multi-agent-mux-monitor` (Kanban worker)",
"- Continuous live tracking → `multi-agent-mux-monitor`"),
])
sub('.agents/skills/multi-agent-mux-create/SKILL.md', [
('If any check fails → `kanban_block(reason="...")` (worker path) or report to user (interactive path).',
"If any check fails → abort with a non-zero exit and report the reason (automated path) or report to user (interactive path)."),
("(e.g., cmux, Kanban workers, manual herdr sessions)",
"(e.g., cmux, background workers, manual herdr sessions)"),
])
sub('README.md', [
("A long-running Kanban reconcile worker that dynamically monitors",
"A long-running reconcile worker that dynamically monitors"),
])
sub('README.ko.md', [
("백그라운드에서 Kanban Reconcile 프로세스로 실행되어",
"백그라운드 Reconcile 프로세스로 실행되어"),
])
실측 출력:
ok .agents/skills/multi-agent-mux-status/SKILL.md (6 edits)
ok .agents/skills/multi-agent-mux-create/SKILL.md (2 edits)
ok README.md (1 edits)
ok README.ko.md (1 edits)
부록 C — G-C 게이트 전문 (.mam/gates/gate_gc.py) 🆕 Rev.2
토큰 목록은 F1 이 삭제하는 텍스트에서 파생한 것이지 손으로 열거한 것이 아니다.
HEAD 에서 61 FAIL, 적용 후 0 PASS 로 red/green 실측 완료.
#!/usr/bin/env python3
"""G-C — Kanban CLI/API surface residue gate.
Token list is DERIVED from the text F1 deletes, not hand-enumerated."""
import sys, glob
KANBAN_SURFACE = [
"kanban", "KANBAN", "Kanban",
"kanban_comment", "kanban_block", "kanban_heartbeat",
"HERMES_KANBAN_TASK", "goal_mode",
"--goal", "--goal-max-turns", "--comment-card",
"--assignee", "--max-runtime", "--max-retries",
"--title", "--branch", "--body", "--skill",
]
# Removed by F1 but NOT Kanban surface: a real env var (reconcile.sh:23) whose doc
# line goes away for a different reason (false .cache claim, plan §8-a). Never
# make it a forbidden word — it may legitimately be re-documented later.
EXCLUDED = {"AGENT_SESSIONS_STATE_DIR"}
targets = sorted(glob.glob('.agents/skills/*/SKILL.md')) + ['README.md', 'README.ko.md']
hits = []
for f in targets:
try:
lines = open(f, encoding='utf-8').read().splitlines()
except FileNotFoundError:
continue
for i, ln in enumerate(lines, 1):
for t in KANBAN_SURFACE:
if t in ln:
hits.append((f, i, t, ln.strip()[:70]))
if hits:
print(f"G-C FAIL — {len(hits)} Kanban-surface hit(s):")
for f, i, t, s in hits:
print(f" {f}:{i} [{t}] {s}")
sys.exit(1)
print("G-C PASS — 0 Kanban CLI/API surface tokens")
호출 시 주의: 실패 판정은 exit code 로 받아야 한다. | head 같은 파이프를 붙이면 파이프라인 종료코드가 마지막 명령의 것으로 바뀌어 FAIL 이 0 으로 보인다.
python3 .mam/gates/gate_gc.py && echo "G-C ok" || echo "G-C FAILED"
파생 절차 재현법 (토큰 목록을 다시 뽑아야 할 때):
import re, subprocess, io
head = subprocess.run(['git','show','HEAD:.agents/skills/multi-agent-mux-monitor/SKILL.md'],
capture_output=True, text=True).stdout
new = io.open('.agents/skills/multi-agent-mux-monitor/SKILL.md', encoding='utf-8').read()
TOK = re.compile(r'--[a-zA-Z][\w-]+|[A-Za-z_][\w]*_[\w]+|\b[A-Z][A-Z0-9_]{3,}\b')
print(sorted(set(TOK.findall(head)) - set(TOK.findall(new))))
이 절차가 산출한 16개 토큰 중 15개가 Kanban 표면, 1개(AGENT_SESSIONS_STATE_DIR)가 제외 대상이다.
[AGREEMENT: REACHED]