From 36b84dd97c5d880003336b346e4d1f70842f9de6 Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:59:00 +0900 Subject: [PATCH 1/8] =?UTF-8?q?docs(spec):=20orchestrate=20ready-set=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20+=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=20=EC=A4=91=20task=20=EB=B6=84=ED=95=A0=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 배리어를 의존 그래프 + 슬롯 회계로 교체하고, 실행 중 task 분할 경로를 여는 설계. brainstorming 결과물이며 writing-plans의 입력이 된다. 핵심 결정: - deps 충족 기준은 approved (impl_done 아님) — 미검토 인터페이스 위에 쌓지 않는다 - 슬롯은 디스패치~종료 상태까지 점유 (리뷰 대기도 포함) — 캡이 보호하는 건 코디네이터 주의력 - 슬롯 수는 코디네이터 제안 + Gate 1 승인, LO_MAX_SESSIONS가 상한 - 분할은 겹치면 같은 워커에 순차 부착, 안 겹치면 새 노드로 병렬 - ready-set.sh exit 3 = 교착 (실패한 의존/사이클) — 조용한 대기와 구분 착지는 PR 2개: 스케줄러 교체, 분할 경로. Co-Authored-By: Claude Opus 5 --- ...-orchestrate-ready-set-scheduler-design.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-07-orchestrate-ready-set-scheduler-design.md diff --git a/docs/superpowers/specs/2026-08-07-orchestrate-ready-set-scheduler-design.md b/docs/superpowers/specs/2026-08-07-orchestrate-ready-set-scheduler-design.md new file mode 100644 index 0000000..13c65ab --- /dev/null +++ b/docs/superpowers/specs/2026-08-07-orchestrate-ready-set-scheduler-design.md @@ -0,0 +1,223 @@ +# orchestrate — ready-set 스케줄러 + 실행 중 task 분할 + +> 상태: **설계 확정** (brainstorming → 이 문서 → writing-plans 순). 2026-08-07. +> 대상: `skills/orchestrate` (v1.4.1 기준). 정본은 `skills/orchestrate/SKILL.md`. + +--- + +## 1. 문제 + +현재 orchestrate는 Phase 2에서 task를 **Wave**로 위상정렬하고, Wave 단위 배리어로 실행한다. +여기서 두 가지가 막힌다. + +**(1) 동시 세션 수가 하드코딩 4다.** SKILL.md에 근거 없이 숫자만 있고, 설정으로 뺄 경로도 +없다. Wave가 캡을 초과하면 "Wave를 쪼개거나 사용자에게 물어봄" — 분해 시점의 정적 결정이다. +실행 중에 조정할 방법이 없다. + +**(2) Wave 배리어가 슬롯을 놀린다.** 한 Wave의 task가 전부 끝나야 다음 Wave가 시작하므로, +느린 task 하나가 나머지 워커를 전부 붙잡는다. 그리고 실행 중에 "이 task가 예상보다 크다"를 +알게 돼도 쪼개서 다른 워커를 투입할 경로가 없다. + +### 캡이 보호하는 자원 (확정) + +측정 가능한 머신 자원이 아니다. 실측·합의된 보호 대상은 둘이다. + +- **API 사용량/예산** — 2026-08-07 3워커 런에서 워커 3개 + 서브에이전트가 동시에 org + monthly spend limit에 걸려 전부 멈췄다. `worker-show`의 state는 그동안에도 `ready`였다. +- **코디네이터 주의력** — 오케스트레이터 세션이 N개 워커의 메일·리뷰·rework를 동시에 들고 + 있어야 한다. + +둘 다 OS에서 조회할 수 없다. 남은 API 예산을 묻는 API는 없고(한도 도달은 transcript +텍스트로만 확인됐다), 주의력은 애초에 기계적 측정 대상이 아니다. **따라서 "동적"은 머신에서 +숫자를 계산하는 것이 아니라, 코디네이터가 명시된 기준으로 판단하고 한도에 부딪히면 반응적으로 +줄이는 것을 뜻한다.** + +## 2. 목표 / 비목표 + +**목표** + +- Wave 배리어를 제거하고, 의존이 충족된 task를 빈 슬롯만큼 흘려보낸다. +- 슬롯 수를 코디네이터가 제안하고 사용자가 Gate 1에서 승인하며, `LO_MAX_SESSIONS`로 상한을 + 고정할 수 있게 한다. +- 실행 중 task 분할 경로를 연다. 파일이 겹치지 않으면 병렬 이득, 겹치면 리뷰 단위 축소 이득. +- 실패한 의존으로 인한 교착을 "조용한 대기"와 구분해 즉시 드러낸다. + +**비목표** + +- 슬롯 할당과 dispatch를 스크립트가 자동 수행하는 것. 판단(어떤 task를 먼저, 분할 승인 + 여부)은 코디네이터가 유지한다. Orca 가이드도 `Agents still choose placement and + concurrency; Orca does not schedule workers`로 같은 층을 그린다. +- 머신 자원(CPU/메모리) 기반 자동 산출. 보호 대상이 아니다. +- Gate 1 / Gate 2의 제거. 사용자 승인 지점은 그대로 둔다. + +## 3. 설계 + +### 3.1 스케줄러 모델 + +`## Waves` 배열이 **의존 그래프 + 슬롯 회계**로 교체된다. Wave는 Gate 1 보고서에서 예상 +흐름을 보여주는 예시로만 남고, 실행 규칙이 아니다. + +**새 산출물 `.orchestration/graph.json`** — Phase 2가 사람용 `conflict-matrix.md`와 **함께** +쓴다. 마크다운 표는 스크립트가 읽을 수 없기 때문이다. + +```json +{ "tasks": [ + { "id": "t1", "deps": [], "files": ["src/auth/**"], "outputs": ["AuthToken"] }, + { "id": "t3", "deps": ["t1"], "files": ["src/api/**"], "consumes": ["AuthToken"] } +] } +``` + +**`ready-set.sh`** — 입력은 `graph.json` + `.orchestration/status/*.json`, 출력은 지금 +디스패치해도 되는 task id 목록. 순수 그래프 연산이므로 bats로 고정할 수 있고, 재진입 시 같은 +상태면 같은 답을 낸다. + +``` +ready = deps 전부 충족 ∧ 자기 phase가 pending (아직 한 번도 디스패치 안 됨) +busy = phase ∉ {pending, approved, merged, done, failed} (디스패치됐고 아직 안 끝남) +free = cap − |busy| +dispatch = ready 중 free 개 +``` + +**슬롯은 디스패치된 순간부터 종료 상태에 도달할 때까지 점유된다.** `planning`/`implementing` +뿐 아니라 `plan_ready`(계획 리뷰 대기)와 `impl_done`(diff 리뷰 대기)도 점유로 센다. 그 +task의 워커는 놀고 있어도 워크트리·세션을 쥐고 있고, 무엇보다 **코디네이터의 주의력을 +점유**하고 있기 때문이다 — 그것이 이 캡이 보호하는 자원이다. 리뷰 대기를 슬롯에서 빼면 +"리뷰 안 된 task 10개가 쌓인 채 새 task가 계속 들어오는" 상태가 되어 캡이 무의미해진다. + +**deps 충족 기준은 `approved`다 — `impl_done`이 아니다.** B가 A의 미검토 인터페이스 위에 +작업을 시작했는데 A가 rework로 시그니처가 바뀌면 B의 작업이 통째로 무효가 된다. 현행 Wave +모델도 실질적으로 이 의미("previous Wave is fully approved")였고 그 안전성은 유지한다. +결과적으로 **전역 배리어가 task별 의존 대기로 좁아지는 것**이지, 배리어가 완전히 사라지는 +것은 아니다. + +### 3.2 슬롯 수 결정 + +코디네이터가 Gate 1에서 숫자를 제안하고, 무엇을 보호하는 값인지(주의력/예산)와 근거(task +수·크기·위험도)를 함께 밝힌다. 사용자가 승인하거나 고친다. + +`LO_MAX_SESSIONS`가 설정돼 있으면 그 값이 **상한**이며 코디네이터의 제안을 덮어쓴다. +`LO_PHASE_TIMEOUTS`와 같은 성격의 노브이고, 같은 `LO_*` 환경변수 계열을 따른다. +`tools.json`에는 넣지 않는다 — 그 파일은 역할(role) 매핑 전용이며, 캡은 역할이 아니다. + +### 3.3 한도/stall 반응 + +`watch-status.sh` exit 7 또는 `orca-worker-stalled.sh`가 stall을 보고하면 **큐 투입만 +중단하고 실행 중인 워커는 건드리지 않는다.** 그리고 사용자에게 보고하고 지시를 기다린다. + +근거: 2026-08-07 런에서 한도 해소 후 워커가 자동 재개됐다. 재기동했다면 오히려 작업을 +잃었을 상황이었다. + +### 3.4 실행 루프 (Phase 3+4 통합) + +Wave마다 Phase 3 → Phase 4를 반복하던 구조가 task 단위 단일 루프가 된다. + +``` +반복: + 1. ready-set.sh → ready 목록 + 빈 슬롯 수 + 2. min(빈슬롯, |ready|) 디스패치 — 이 시점에 brief 작성 + 3. 이벤트 대기 → orca-wait.sh / watch-status.sh + 4. impl_done → diff 리뷰 → approved 또는 rework 주입 + 5. 전 task approved → Phase 5 (통합 테스트) +``` + +**brief 주입 시점이 정확해진다.** 지금은 Wave 시작 때 선행 Wave의 인터페이스를 한꺼번에 +주입하지만, 앞으로는 각 task를 **디스패치하는 순간** 그 task가 실제로 consume하는 선행 +시그니처만 넣는다. 그 시점에 선행은 이미 `approved`라 시그니처가 확정돼 있다. + +**`watch-status.sh`에 any-도달 모드가 필요하다.** 현재 계약은 "N개가 목표 phase에 전부 +도달하면 exit 0"인데, 슬롯 스케줄러는 "추적 중인 task 중 하나라도 도달하면 즉시 반환"이 +필요하다 — 4개 중 1개만 끝나도 빈 슬롯을 채워야 하기 때문이다. Orca 쪽 `orca-wait.sh`는 +이미 이벤트 기반(worker_done 하나만 와도 반환)이라 그대로 쓴다. 이 비대칭이 이번 작업에서 +가장 손이 많이 가는 부분이다. + +**재진입은 단순해진다.** "어느 Wave 중간인지"를 복원할 필요가 없어지고, `graph.json` + +`status/*.json`을 읽어 `ready-set.sh`를 돌리면 끝이다. Wave 인덱스 같은 중간 상태가 없다. + +### 3.5 실행 중 task 분할 + +**트리거는 기존 채널을 쓴다.** 워커가 plan 단계나 구현 중에 판단하면 Orca는 `ask`, tmux는 +`ask-coordinator.sh`로 올린다. 새 채널을 만들지 않는다. + +**워커는 제안에 파일 범위를 반드시 붙인다** — 조각 각각이 어떤 파일을 만지고 무엇을 +export하는지. 없으면 코디네이터가 충돌 판정을 할 수 없어 제안이 판단 불가가 된다. brief +템플릿의 ask 규칙에 이 요구사항을 명시한다. + +**코디네이터의 판정은 충돌 매트릭스 한 번이다.** + +| 겹침 | 처리 | 얻는 것 | +|---|---|---| +| 안 겹침 | `graph.json`에 새 노드, 새 워크트리, ready-set 진입 → 빈 슬롯이 집어감 | 병렬성 | +| 겹침 | 같은 워크트리·같은 워커에 후속 task로 부착 (`deps: [parent]`) | 리뷰/rework 단위 축소 | + +겹치는 경우 별도 워크트리를 만들지 않는 것이 중요하다. B가 A의 파일을 편집해야 하는데 A의 +코드는 Phase 6 전까지 통합 브랜치에 없다. 현재 의존 처리는 **인터페이스 시그니처 주입**이라 +"B가 다른 파일을 쓰면서 A의 export를 소비"할 때만 성립하고, 같은 파일을 편집하는 분할에는 +통하지 않는다. 겹치면 같은 워크트리에서 같은 워커가 이어서 하며, 이는 이미 있는 +메커니즘이다 — Orca는 `--terminal ` 재사용, tmux는 세션 재사용. + +**분할 깊이는 1로 제한한다.** 쪼개서 나온 조각은 다시 쪼갤 수 없다. 재귀적 분할로 일을 +미루는 경로를 막고, `graph.json`이 사람이 못 읽는 물건이 되는 것을 막는다. 더 깊이 필요하면 +Phase 2 분해가 틀렸다는 신호이므로 사용자에게 올린다. + +**거절도 명시적 응답이어야 한다.** 코디네이터가 "쪼개지 말고 끝내라"고 판단하면 반드시 +`reply`를 보낸다. 답이 없으면 워커가 자체 판단으로 진행한다 — v1.4.1에서 고친 ask-타임아웃 +문제가 정확히 여기서 재발할 수 있다. + +**분할 승인은 코디네이터가 결정하고 즉시 보고한다.** 사용자의 블로킹 승인을 받지 않는다. +Gate 1에서 승인한 task 목록이 늘어나는 것은 맞지만, 분할마다 사람을 기다리면 이 설계가 +없애려던 정체가 그대로 돌아온다. 대신 판정 근거(겹침 여부, 스케줄 변화)를 즉시 보고해 +사용자가 개입할 수 있게 한다. + +## 4. 실패 처리 + +**이 모델이 새로 만드는 실패 모드가 있다.** Wave 모델에서는 task 실패가 그 Wave를 눈에 띄게 +멈췄다. ready-set에서는 실패한 task의 의존자들이 영영 ready에 나타나지 않고, 루프는 "지금은 +던질 게 없네"로 읽어 조용히 대기한다. v1.4.1에서 `orca-wait.sh`에 고친 것과 같은 종류의 +버그다 — 조용한 것과 고장난 것이 구분되지 않는. + +`ready-set.sh`는 네 상태를 구분한다. + +| 종료코드 | 의미 | 코디네이터 행동 | +|---|---|---| +| 0 | 던질 task 있음 (stdout에 id 목록) | 디스패치 | +| 2 | 던질 것 없음, 실행 중인 워커 있음 | 이벤트 대기 | +| 3 | 던질 것도 실행 중인 것도 없는데 미완료 task가 남음 | **교착** — 실패한 의존 또는 사이클. 대기 금지, 즉시 보고 | +| 4 | `graph.json`/status를 못 읽음 | 거부. 추측 금지 | +| 5 | 전 task가 종료 상태 | Phase 5로 진행 | + +**3번이 핵심 안전장치다.** 없으면 실패 하나가 전체 오케스트레이션을 무한 대기로 몬다. + +종료코드는 이 스크립트의 자체 계약이다(`watch-status.sh`의 4와 `orca-wait.sh`의 4가 이미 +서로 다른 의미인 것과 같다). 각 코드의 뜻은 스크립트 헤더에 적는다. + +**graph 검증** — 사이클 검사, 분할 깊이 1 초과 거부, 중복 output 검사는 **Phase 2의 최초 +`graph.json` 작성 시점과 분할로 인한 변경 시점 양쪽에서** 같은 규칙으로 돈다. 분해가 처음부터 +사이클을 만들 수도 있기 때문이다. 하나라도 걸리면 변경을 적용하지 않고 거부 사유를 보고한다 +— 반쯤 적용된 그래프가 남으면 재진입이 깨진다. 검증을 빠져나간 사이클은 런타임에 +`ready-set.sh`의 exit 3이 잡는다(두 번째 방어선). + +## 5. 테스트 + +`ready-set.sh` — bats: + +- 정상: 의존 충족 → 디스패치, 빈 슬롯 수만큼만 +- 에러: 깨진 JSON → 4, 사이클 → 3 +- 경계값: 빈 그래프, 전부 완료(5), task 1개, ready > 슬롯, **실패한 의존을 가진 task → 3** +- 슬롯 회계: `LO_MAX_SESSIONS` 상한이 코디네이터 제안을 덮어쓰는지 + +`watch-status.sh` — any-도달 모드 신규 테스트 + **기존 all-N 모드 무회귀**. + +분할 — 겹침 → `deps:[parent]`, 안 겹침 → `deps:[]`, 깊이 2 시도 → 거부, 사이클 유발 → 거부. + +기존 387개 bats는 전부 그린을 유지한다. PR 통과 조건이다. + +## 6. 착지 순서 + +**PR 1 — 스케줄러 교체**: `graph.json`, `ready-set.sh`, `watch-status.sh` any-도달 모드, +Phase 2/3/4 재작성, 재진입, 슬롯 제안 + `LO_MAX_SESSIONS`. + +**PR 2 — 분할 경로**: 워커 ask 규칙(파일 범위 필수), 코디네이터 판정, graph 변경 + 방어, +겹침 시 같은 워커 부착. + +두 PR로 끊는 이유: 스케줄러 교체만으로도 Phase 3·4 전면 재작성과 재진입·brief 주입 시점이 +바뀐다. 여기에 분할까지 한 커밋에 넣으면 문제가 생겼을 때 원인을 가릴 수 없다. From 112b45bc653477685507941a5fd123bb468c9e6b Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:13:51 +0900 Subject: [PATCH 2/8] =?UTF-8?q?docs(plan):=20PR=201=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EA=B3=84=ED=9A=8D=20=E2=80=94=20ready-set=20=EC=8A=A4?= =?UTF-8?q?=EC=BC=80=EC=A4=84=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit task 4개: ready-set.sh 신규(13 bats), watch-status --tasks 스코프, Phase 2 재작성(graph.json + 슬롯 제안), Phase 3+4 디스패치 루프 + 재진입. 스펙 §3.3(한도/stall 반응)은 기존 exit 7 경로로 충족되어 새 코드가 없다는 점을 self-review에 근거와 함께 남겼다. --- ...6-08-07-orchestrate-ready-set-scheduler.md | 673 ++++++++++++++++++ 1 file changed, 673 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md diff --git a/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md b/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md new file mode 100644 index 0000000..91085e1 --- /dev/null +++ b/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md @@ -0,0 +1,673 @@ +# orchestrate ready-set 스케줄러 (PR 1) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** orchestrate의 Wave 배리어를 의존 그래프 + 슬롯 회계로 교체해, 의존이 충족된 task를 빈 슬롯만큼 즉시 흘려보낸다. + +**Architecture:** Phase 2가 기계 판독용 `.orchestration/graph.json`을 쓰고, 새 `ready-set.sh`가 그래프 + `.orchestration/status/*.json`을 읽어 "지금 던져도 되는 task id"를 종료코드로 판정한다. 판단(슬롯 수, 어떤 task부터)은 코디네이터가 유지한다. tmux 기판의 `watch-status.sh`는 스캔 대상을 task 집합으로 좁히는 `--tasks` 옵션을 얻어 "추적 중인 것 중 아무나 하나 도달"을 표현할 수 있게 된다. + +**Tech Stack:** POSIX sh, jq, bats-core. 새 의존성 없음. + +## Global Constraints + +- 스펙 정본: `docs/superpowers/specs/2026-08-07-orchestrate-ready-set-scheduler-design.md`. 각 task는 해당 절을 권위로 한다. +- deps 충족 기준은 `approved` 이상이다 — `impl_done`이 아니다 (스펙 §3.1). +- 슬롯은 디스패치된 순간부터 종료 상태까지 점유한다. `plan_ready`·`impl_done`(리뷰 대기)도 점유로 센다 (스펙 §3.1). +- 종료 상태 = `approved | merged | done | failed`. 그 중 `failed`는 "완료"가 아니라 교착 사유다. +- 새 스크립트는 POSIX `sh`로 쓰고 `set -u`를 켠다. `jq` 부재는 exit 127 (`watch-status.sh`·`orca-wait.sh`와 동일). +- `/tmp`·`$TMPDIR`에 실행 파일을 만들지 않는다. 테스트용 실행 파일이 필요하면 레포 안 `.claude/tmp/`를 쓴다 (`tests/orca-wait.bats`의 `REPO_TMP()` 선례). +- 기존 387개 bats는 전부 그린을 유지한다. PR 통과 조건이다. +- 분할 경로(스펙 §3.5)는 PR 2다. 이 계획에서 구현하지 않는다. +- 커밋 author는 `Younggi Choi <74581798+choiyounggi@users.noreply.github.com>` (공개 레포 관례). + +--- + +## File Structure + +**신규** + +- `skills/orchestrate/scripts/ready-set.sh` — 그래프 + status를 읽어 디스패치 가능 집합을 판정. 이 PR의 유일한 신규 실행 파일. 순수 판정만 하고 아무것도 실행하지 않는다. +- `tests/ready-set.bats` — 위 스크립트의 계약 고정. + +**수정** + +- `skills/orchestrate/scripts/watch-status.sh` — `--tasks ` 옵션 추가 (스캔 대상 축소). 기존 위치 인자 계약은 그대로. +- `tests/scripts.bats` — `--tasks` 신규 테스트 + 기존 all-N 무회귀. +- `skills/orchestrate/SKILL.md` — Phase 2(graph.json 산출 + 슬롯 제안), Phase 3+4(디스패치 루프), 재진입. +- `tests/send-prompt.bats` — SKILL.md 앵커 테스트가 있는 파일. Phase 재작성으로 앵커가 깨지지 않는지 확인하고, 필요하면 갱신. + +--- + +### Task 1: `ready-set.sh` — 디스패치 가능 집합 판정 + +**Files:** +- Create: `skills/orchestrate/scripts/ready-set.sh` +- Test: `tests/ready-set.bats` + +**Interfaces:** +- Consumes: `.orchestration/graph.json` (이 task가 스키마를 정의), `.orchestration/status/.json`의 `.phase` 필드 (`status-update.sh`가 쓰는 기존 형식). +- Produces: CLI 계약 `ready-set.sh ` — stdout은 디스패치할 task id 한 줄에 하나. 종료코드 0/2/3/4/5. Task 3·4의 SKILL.md가 이 계약을 인용한다. + +**graph.json 스키마** (이 task가 정본): + +```json +{ "tasks": [ + { "id": "t1", "deps": [], "files": ["src/auth/**"], "outputs": ["AuthToken"] }, + { "id": "t3", "deps": ["t1"], "files": ["src/api/**"], "consumes": ["AuthToken"] } +] } +``` + +`id`와 `deps`만 이 스크립트가 읽는다. `files`/`outputs`/`consumes`는 코디네이터가 충돌 판정에 쓰는 필드이며 PR 2에서 사용한다. + +- [ ] **Step 1: 실패하는 테스트를 쓴다 (정상 경로)** + +`tests/ready-set.bats` 생성: + +```bash +#!/usr/bin/env bats +# Tests for ready-set.sh — the Wave-barrier replacement. Given the dependency +# graph and each task's recorded phase, it answers exactly one question: +# which tasks may be dispatched right now. It never launches anything. + +setup() { + RS="${BATS_TEST_DIRNAME}/../skills/orchestrate/scripts/ready-set.sh" + G="${BATS_TEST_TMPDIR}/graph.json" + S="${BATS_TEST_TMPDIR}/status" + mkdir -p "$S" +} + +graph() { printf '%s' "$1" > "$G"; } +phase() { printf '{"task":"%s","phase":"%s"}' "$1" "$2" > "$S/$1.json"; } + +@test "no deps, empty status: every task is dispatchable up to the cap" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":[]},{"id":"t3","deps":[]}]}' + run sh "$RS" "$G" "$S" 2 + [ "$status" -eq 0 ] + [ "$(echo "$output" | grep -c .)" -eq 2 ] +} +``` + +- [ ] **Step 2: 실패를 확인한다** + +Run: `cd ~/Desktop/workspace/dev-loop && npx bats tests/ready-set.bats` +Expected: FAIL — `ready-set.sh` 가 없어서 `sh: ... No such file`. + +- [ ] **Step 3: 최소 구현** + +`skills/orchestrate/scripts/ready-set.sh` 생성: + +```sh +#!/bin/sh +# ready-set.sh — which tasks may be dispatched right now? +# +# This is the Wave-barrier replacement. Waves made the whole batch wait for its +# slowest member; here a task is dispatchable the moment its own dependencies +# are approved and a slot is free, so a finished worker is refilled immediately. +# +# usage: ready-set.sh +# {"tasks":[{"id":"t1","deps":["t0"]}, ...]} — Phase 2 writes it. +# Only `id` and `deps` are read here; files/outputs/consumes are +# the coordinator's conflict-matrix fields. +# .orchestration/status — one .json per task, `.phase` +# written by status-update.sh. A task with no file is `pending`. +# the coordinator's approved slot count. LO_MAX_SESSIONS, when +# set, is an UPPER BOUND on it, never a raise. +# +# exit 0 dispatch these (stdout: one task id per line, at most of them) +# exit 2 nothing to dispatch, but tasks are in flight — wait for an event +# exit 3 nothing to dispatch, nothing in flight, unfinished tasks remain — +# DEADLOCK (a failed dependency, or a cycle). Never wait on this: with +# no worker running, no event can ever arrive. Report it. +# exit 4 the graph or the status dir could not be read, or an argument is +# invalid — refuse rather than guess +# exit 5 every task is in a terminal state — the run is complete +# exit 127 jq not found +# +# A dependency counts as satisfied only at `approved` or higher, NOT at +# impl_done: a task that consumes an unreviewed interface has to be redone when +# rework changes that signature. This is the Wave model's "previous Wave fully +# approved" guarantee, narrowed from a global barrier to a per-task wait. +# +# env: +# LO_MAX_SESSIONS upper bound on (a tuning knob like LO_PHASE_TIMEOUTS) +set -u + +JQ=$(command -v jq) || { echo "ready-set: jq not found" >&2; exit 127; } + +graph="${1:-}"; sdir="${2:-}"; cap="${3:-}" +[ -n "$graph" ] && [ -n "$sdir" ] && [ -n "$cap" ] || { + echo "usage: ready-set.sh " >&2; exit 4; } +[ -f "$graph" ] || { echo "ready-set: graph '$graph' not found" >&2; exit 4; } +[ -d "$sdir" ] || { echo "ready-set: status dir '$sdir' not found" >&2; exit 4; } + +case "$cap" in ''|*[!0-9]*) echo "ready-set: cap must be a positive integer" >&2; exit 4 ;; esac +[ "$cap" -gt 0 ] || { echo "ready-set: cap must be > 0" >&2; exit 4; } + +# LO_MAX_SESSIONS caps the cap. It is a ceiling the operator sets, so it lowers +# the coordinator's proposal and never raises it. +if [ -n "${LO_MAX_SESSIONS:-}" ]; then + case "$LO_MAX_SESSIONS" in + ''|*[!0-9]*) echo "ready-set: LO_MAX_SESSIONS must be a positive integer" >&2; exit 4 ;; + esac + [ "$LO_MAX_SESSIONS" -gt 0 ] || { echo "ready-set: LO_MAX_SESSIONS must be > 0" >&2; exit 4; } + [ "$LO_MAX_SESSIONS" -lt "$cap" ] && cap="$LO_MAX_SESSIONS" +fi + +ids=$("$JQ" -r '.tasks[]?.id // empty' "$graph" 2>/dev/null) || { + echo "ready-set: graph '$graph' is not valid JSON" >&2; exit 4; } + +# `.tasks` absent is a malformed graph, not an empty one — tell them apart so a +# typo'd key cannot read as "nothing to do". +"$JQ" -e 'has("tasks") and (.tasks | type == "array")' "$graph" >/dev/null 2>&1 || { + echo "ready-set: graph '$graph' has no .tasks array" >&2; exit 4; } + +phase_of() { # $1 = task id -> its recorded phase, or "pending" when unrecorded + f="$sdir/$1.json" + [ -f "$f" ] || { echo pending; return; } + p=$("$JQ" -r '.phase // "pending"' "$f" 2>/dev/null) || p=pending + [ -n "$p" ] || p=pending + echo "$p" +} + +is_satisfied() { # $1 = phase -> 0 when a dependent may start on it + case "$1" in approved|merged|done) return 0 ;; *) return 1 ;; esac +} +is_terminal() { # $1 = phase -> 0 when the task will never occupy a slot again + case "$1" in approved|merged|done|failed) return 0 ;; *) return 1 ;; esac +} + +busy=0; unfinished=0; ready="" +for id in $ids; do + ph=$(phase_of "$id") + if is_terminal "$ph"; then + # `failed` is terminal for scheduling but is NOT completion: it leaves its + # dependents permanently unreachable, which is what exit 3 exists to report. + [ "$ph" = failed ] && unfinished=$((unfinished + 1)) + continue + fi + unfinished=$((unfinished + 1)) + if [ "$ph" != pending ]; then busy=$((busy + 1)); continue; fi + + deps=$("$JQ" -r --arg id "$id" '.tasks[] | select(.id == $id) | .deps[]? // empty' "$graph" 2>/dev/null) + ok=1 + for d in $deps; do + # A dep naming a task the graph does not define is a malformed graph, not an + # unsatisfied edge — refuse instead of silently blocking that task forever. + echo "$ids" | grep -qx "$d" || { echo "ready-set: task '$id' depends on unknown '$d'" >&2; exit 4; } + is_satisfied "$(phase_of "$d")" || { ok=0; break; } + done + [ "$ok" = 1 ] && ready="$ready $id" +done + +[ "$unfinished" -eq 0 ] && { echo "[ready-set] all tasks terminal"; exit 5; } + +free=$((cap - busy)) +[ "$free" -lt 0 ] && free=0 + +n=0 +for id in $ready; do + [ "$n" -ge "$free" ] && break + echo "$id"; n=$((n + 1)) +done +[ "$n" -gt 0 ] && exit 0 + +[ "$busy" -gt 0 ] && { echo "[ready-set] nothing dispatchable, $busy in flight — wait" >&2; exit 2; } + +echo "[ready-set] DEADLOCK: $unfinished task(s) unfinished, none dispatchable, none running — a failed dependency or a cycle. Inspect the graph and status; do not wait." >&2 +exit 3 +``` + +`chmod +x skills/orchestrate/scripts/ready-set.sh` + +- [ ] **Step 4: 테스트 통과 확인** + +Run: `npx bats tests/ready-set.bats` +Expected: PASS (1/1) + +- [ ] **Step 5: 나머지 계약을 테스트로 고정한다** + +`tests/ready-set.bats`에 이어서 추가: + +```bash +@test "a dependent waits until its dep is approved, not merely impl_done" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":["t1"]}]}' + phase t1 impl_done + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 2 ] # t1 busy (review pending), t2 not yet startable + + phase t1 approved + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 0 ] + [ "$output" = "t2" ] +} + +@test "review-pending phases occupy a slot (plan_ready and impl_done both count)" { + graph '{"tasks":[{"id":"a","deps":[]},{"id":"b","deps":[]},{"id":"c","deps":[]}]}' + phase a plan_ready + phase b impl_done + run sh "$RS" "$G" "$S" 2 + [ "$status" -eq 2 ] # cap 2 fully occupied by two review-waiting tasks +} + +@test "a failed dependency is a deadlock, never a quiet wait (the core guard)" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":["t1"]}]}' + phase t1 failed + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 3 ] + [[ "$output" == *"DEADLOCK"* ]] +} + +@test "a cycle surfaces as the same deadlock (boundary)" { + graph '{"tasks":[{"id":"t1","deps":["t2"]},{"id":"t2","deps":["t1"]}]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 3 ] +} + +@test "all tasks terminal: exit 5, the run is complete (boundary)" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":["t1"]}]}' + phase t1 approved + phase t2 merged + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 5 ] +} + +@test "an empty task array is complete, not a deadlock (boundary)" { + graph '{"tasks":[]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 5 ] +} + +@test "a graph with no .tasks array is refused, not read as empty (error)" { + graph '{"nodes":[]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 4 ] +} + +@test "malformed JSON is refused (error)" { + graph '}{ not json' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 4 ] +} + +@test "a dep naming an undefined task is refused, not blocked forever (error)" { + graph '{"tasks":[{"id":"t1","deps":["ghost"]}]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 4 ] +} + +@test "LO_MAX_SESSIONS lowers the cap but never raises it" { + graph '{"tasks":[{"id":"a","deps":[]},{"id":"b","deps":[]},{"id":"c","deps":[]}]}' + run env LO_MAX_SESSIONS=1 sh "$RS" "$G" "$S" 3 + [ "$status" -eq 0 ] + [ "$(echo "$output" | grep -c .)" -eq 1 ] + + run env LO_MAX_SESSIONS=9 sh "$RS" "$G" "$S" 2 + [ "$(echo "$output" | grep -c .)" -eq 2 ] +} + +@test "a non-numeric or zero cap is refused (boundary)" { + graph '{"tasks":[{"id":"a","deps":[]}]}' + run sh "$RS" "$G" "$S" 0 + [ "$status" -eq 4 ] + run sh "$RS" "$G" "$S" abc + [ "$status" -eq 4 ] +} + +@test "a missing graph or status dir is refused (error)" { + run sh "$RS" "$BATS_TEST_TMPDIR/nope.json" "$S" 2 + [ "$status" -eq 4 ] + graph '{"tasks":[]}' + run sh "$RS" "$G" "$BATS_TEST_TMPDIR/nodir" 2 + [ "$status" -eq 4 ] +} +``` + +- [ ] **Step 6: 전체 실행** + +Run: `npx bats tests/ready-set.bats` +Expected: PASS (13/13). 실패하면 스크립트를 고친다 — 테스트를 약화시키지 않는다. + +- [ ] **Step 7: 커밋** + +```bash +git add skills/orchestrate/scripts/ready-set.sh tests/ready-set.bats +git -c user.name="Younggi Choi" -c user.email="74581798+choiyounggi@users.noreply.github.com" \ + commit -m "feat(orchestrate): ready-set.sh — 의존 그래프 + 슬롯 회계로 디스패치 가능 집합 판정 + +Wave 배리어 교체의 판정 절반. deps 충족은 approved 이상에서만 성립하고, +리뷰 대기(plan_ready/impl_done)도 슬롯을 점유한다. 실패한 의존으로 인한 +교착은 exit 3으로 즉시 드러나며 조용한 대기(exit 2)와 구분된다." +``` + +--- + +### Task 2: `watch-status.sh --tasks` — 추적 대상 축소 + +**Files:** +- Modify: `skills/orchestrate/scripts/watch-status.sh:36` (인자 파싱), `:129` (스캔 루프) +- Test: `tests/scripts.bats` + +**Interfaces:** +- Consumes: 없음 (독립 변경) +- Produces: `watch-status.sh [--tasks ] [timeout] [interval]`. Task 4의 SKILL.md가 `--tasks impl_done 1` 형태로 인용한다. + +**왜 새 모드가 아니라 스코프인가:** 슬롯 스케줄러가 필요한 건 "추적 중인 것 중 아무나 하나 도달"이다. `done_count`는 status 디렉토리 **전체**를 세므로, 이미 approved된 이전 task들 때문에 `expected=1`이 즉시 만족되어 스핀한다. 스캔을 현재 busy한 id로 좁히면 `expected=1`이 정확히 "아무나 하나"가 된다. + +- [ ] **Step 1: 실패하는 테스트를 쓴다** + +`tests/scripts.bats` 끝에 추가: + +```bash +@test "watch-status --tasks: only the named tasks are counted" { + mkdir -p "$STATUS_DIR" + printf '{"task":"old","phase":"approved"}' > "$STATUS_DIR/old.json" + printf '{"task":"a","phase":"implementing"}' > "$STATUS_DIR/a.json" + # `old` already passed the target, but it is not being tracked: without + # scoping, expected=1 would be satisfied instantly and the wait would spin. + run bash "$WS" --tasks a "$STATUS_DIR" impl_done 1 2 1 + [ "$status" -eq 2 ] +} + +@test "watch-status --tasks: returns as soon as ANY tracked task reaches target" { + mkdir -p "$STATUS_DIR" + printf '{"task":"a","phase":"impl_done"}' > "$STATUS_DIR/a.json" + printf '{"task":"b","phase":"implementing"}' > "$STATUS_DIR/b.json" + run bash "$WS" --tasks a,b "$STATUS_DIR" impl_done 1 5 1 + [ "$status" -eq 0 ] +} + +@test "watch-status --tasks: an untracked failed task does not abort the wait" { + mkdir -p "$STATUS_DIR" + printf '{"task":"old","phase":"failed"}' > "$STATUS_DIR/old.json" + printf '{"task":"a","phase":"implementing"}' > "$STATUS_DIR/a.json" + run bash "$WS" --tasks a "$STATUS_DIR" impl_done 1 2 1 + [ "$status" -eq 2 ] +} + +@test "watch-status --tasks: an explicit timeout argument still outranks the env" { + # --tasks is consumed before the positional count is taken; if argc were + # captured before that, the 4th positional would stop being recognised and + # LO_PHASE_TIMEOUTS would silently win. + mkdir -p "$STATUS_DIR" + printf '{"task":"a","phase":"implementing"}' > "$STATUS_DIR/a.json" + run env LO_PHASE_TIMEOUTS="impl_done=999" bash "$WS" --tasks a "$STATUS_DIR" impl_done 1 2 1 + [[ "$output" == *"budget=2s"* ]] + [[ "$output" == *"source=arg"* ]] +} + +@test "watch-status: without --tasks every status file is still counted (no regression)" { + mkdir -p "$STATUS_DIR" + printf '{"task":"a","phase":"done"}' > "$STATUS_DIR/a.json" + printf '{"task":"b","phase":"done"}' > "$STATUS_DIR/b.json" + run bash "$WS" "$STATUS_DIR" impl_done 2 5 1 + [ "$status" -eq 0 ] +} +``` + +- [ ] **Step 2: 실패를 확인한다** + +Run: `npx bats tests/scripts.bats` +Expected: 새 테스트 4개 FAIL (`--tasks`가 디렉토리 인자로 읽혀 exit 4). 마지막 무회귀 테스트는 PASS. + +- [ ] **Step 3: 인자 파싱을 고친다** + +`watch-status.sh`에서 `argc=$#` 줄(현재 35–36행)을 다음으로 교체: + +```sh +# --tasks scopes the scan to the given ids. The slot scheduler needs "any ONE of +# the tasks I am currently running reached the target"; counting the whole status +# dir would satisfy expected=1 from tasks approved in earlier rounds and spin. +only="" +while [ $# -gt 0 ]; do + case "$1" in + --tasks) shift; only="${1:-}"; [ -n "$only" ] || { echo "watch-status: --tasks needs a comma-separated id list" >&2; exit 4; }; shift ;; + --) shift; break ;; + -*) echo "watch-status: unknown option '$1'" >&2; exit 4 ;; + *) break ;; + esac +done +# Captured AFTER option parsing: argc decides whether the 4th POSITIONAL was +# given, and options must not be counted toward it. +argc=$# +dir="$1"; target="$2"; expected="$3"; timeout="${4:-3600}"; interval="${5:-15}" +``` + +- [ ] **Step 4: 스캔 루프에 필터를 넣는다** + +`for f in "$dir"/*.json; do` 바로 다음 `[ -f "$f" ] || continue` 아래에 추가: + +```sh + if [ -n "$only" ]; then + base=${f##*/}; base=${base%.json} + # Exact membership on a comma-delimited list: the commas around both sides + # keep `t1` from matching `t12`. + case ",$only," in *",$base,"*) : ;; *) continue ;; esac + fi +``` + +- [ ] **Step 5: 테스트 통과 확인** + +Run: `npx bats tests/scripts.bats` +Expected: PASS (전부) + +- [ ] **Step 6: 헤더 문서 갱신** + +`watch-status.sh` 헤더의 usage 줄을 `watch-status.sh [--tasks ] [timeout] [interval]`로 고치고, `--tasks`의 목적(슬롯 스케줄러의 "아무나 하나" 대기)을 두 줄로 적는다. + +- [ ] **Step 7: 전체 스위트 + 커밋** + +Run: `npx bats tests/` → 실패 0, 통과 수가 이전보다 늘었는지 확인. + +```bash +git add skills/orchestrate/scripts/watch-status.sh tests/scripts.bats +git -c user.name="Younggi Choi" -c user.email="74581798+choiyounggi@users.noreply.github.com" \ + commit -m "feat(orchestrate): watch-status --tasks — 추적 중인 집합으로 스캔 축소 + +슬롯 스케줄러는 '지금 돌리는 것 중 아무나 하나 도달'이 필요한데, status 디렉토리 +전체를 세면 이전 라운드의 approved가 expected=1을 즉시 만족시켜 스핀한다. +argc는 옵션 파싱 뒤에 잡아 4번째 위치 인자 판정이 깨지지 않게 한다." +``` + +--- + +### Task 3: SKILL.md Phase 2 — graph.json 산출 + 슬롯 제안 + +**Files:** +- Modify: `skills/orchestrate/SKILL.md:73-107` (Phase 2 + Gate 1) +- Test: `tests/scripts.bats` (문서 계약 grep — `tests/send-prompt.bats:467`의 앵커 테스트 선례를 따른다) + +**Interfaces:** +- Consumes: Task 1의 `graph.json` 스키마와 `ready-set.sh` CLI 계약. +- Produces: Gate 1 보고서 형식(슬롯 수 + 근거). Task 4의 디스패치 루프가 이 승인된 cap을 소비한다. + +- [ ] **Step 1: 문서 계약 테스트를 먼저 쓴다** + +`tests/scripts.bats` 끝에 추가: + +```bash +@test "SKILL.md Phase 2: graph.json artifact and the slot proposal are documented" { + SKILL="${BATS_TEST_DIRNAME}/../skills/orchestrate/SKILL.md" + grep -qF '.orchestration/graph.json' "$SKILL" + grep -qF 'LO_MAX_SESSIONS' "$SKILL" + # The cap must never be a bare number again: the report has to name what it + # protects, or "dynamic" degrades back into a hardcoded 4. + grep -qF '코디네이터 주의력' "$SKILL" + ! grep -qF 'concurrent-session cap** (default 4)' "$SKILL" +} +``` + +- [ ] **Step 2: 실패 확인** + +Run: `npx bats tests/scripts.bats` +Expected: FAIL — `graph.json`이 SKILL.md에 없음. + +- [ ] **Step 3: Phase 2를 고쳐 쓴다** + +`## Phase 2 — Decompose`에서 `Apply a **concurrent-session cap** (default 4) — if a Wave exceeds it, split it or ask. Tasks in the same Wave are independent (parallel); a later Wave starts only after the previous Wave is approved.` 문장을 삭제하고, 그 자리에 넣는다: + +```markdown +그리고 두 산출물을 **함께** 쓴다: 사람용 `conflict-matrix.md`와 기계용 +`.orchestration/graph.json`. 마크다운 표는 스케줄러가 읽을 수 없다. + +```json +{ "tasks": [ + { "id": "t1", "deps": [], "files": ["src/auth/**"], "outputs": ["AuthToken"] }, + { "id": "t3", "deps": ["t1"], "files": ["src/api/**"], "consumes": ["AuthToken"] } +] } +``` + +Wave는 **실행 단위가 아니라 Gate 1 보고서의 예시**다. 실행은 `ready-set.sh`가 +판정한다: 의존이 `approved` 이상인 task를 빈 슬롯만큼 흘려보낸다. 위상정렬은 여전히 +하되, 그 결과는 "이런 순서로 흐를 겁니다"를 보여주기 위한 것이다. + +**슬롯 수를 제안한다.** task 수·크기·위험도를 근거로 숫자를 정하고, **무엇을 보호하는 +값인지 함께 밝힌다** — 이 캡이 지키는 것은 머신 자원이 아니라 **코디네이터 주의력**과 +**API 사용량/예산**이다(둘 다 OS에서 조회할 수 없어 판단으로 정한다). 슬롯은 디스패치된 +순간부터 종료 상태까지 점유되며, `plan_ready`/`impl_done` 같은 리뷰 대기도 점유로 센다 — +리뷰 안 된 task가 쌓인 채 새 task가 들어오면 캡이 무의미해지기 때문이다. +`LO_MAX_SESSIONS`가 설정돼 있으면 그 값이 상한이며 제안을 덮어쓴다. +``` + +`graph.json`을 쓴 직후 검증한다는 문장을 덧붙인다: + +```markdown +쓴 뒤 `scripts/ready-set.sh `를 한 번 돌려 **exit 4가 아닌지** +확인한다 — 깨진 JSON·`.tasks` 누락·정의되지 않은 의존을 여기서 잡는다. +``` + +- [ ] **Step 4: Gate 1 보고 항목을 고친다** + +`## 🚦 Gate 1` 의 `Report the task list, Waves, session count, and a rough cost note.` 를 다음으로 교체: + +```markdown +Report the task list, the dependency graph (예상 흐름을 Wave 형태로 보여줘도 좋다), the +proposed **slot count with its rationale and what it protects**, and a rough cost note. +``` + +- [ ] **Step 5: 테스트 통과 확인** + +Run: `npx bats tests/scripts.bats` +Expected: PASS + +- [ ] **Step 6: 커밋** + +```bash +git add skills/orchestrate/SKILL.md tests/scripts.bats +git -c user.name="Younggi Choi" -c user.email="74581798+choiyounggi@users.noreply.github.com" \ + commit -m "docs(orchestrate): Phase 2 — graph.json 산출 + 근거 있는 슬롯 제안 + +하드코딩 4를 없애고, 캡이 보호하는 자원(코디네이터 주의력/API 예산)을 보고에 +명시하게 한다. LO_MAX_SESSIONS가 상한." +``` + +--- + +### Task 4: SKILL.md Phase 3+4 → 디스패치 루프, 재진입 + +**Files:** +- Modify: `skills/orchestrate/SKILL.md:109-141` (Phase 3 서두), `:188-213` (O4), `:332-350` (Phase 4), `:378-400` (재진입) +- Test: `tests/scripts.bats` + +**Interfaces:** +- Consumes: Task 1의 `ready-set.sh` 종료코드 0/2/3/4/5, Task 2의 `watch-status.sh --tasks`, Task 3의 승인된 cap. +- Produces: 없음 (PR 1의 마지막 task) + +- [ ] **Step 1: 문서 계약 테스트를 먼저 쓴다** + +```bash +@test "SKILL.md: the dispatch loop cites ready-set exit codes and --tasks" { + SKILL="${BATS_TEST_DIRNAME}/../skills/orchestrate/SKILL.md" + grep -qF 'scripts/ready-set.sh' "$SKILL" + grep -qF '--tasks' "$SKILL" + # exit 3 is the guard this design turns on; it must be spelled out as + # "do not wait", or it degrades into the silent stall it exists to prevent. + grep -qF '교착' "$SKILL" + # Waves must no longer be described as an execution barrier. + ! grep -qF 'A later Wave launches only after' "$SKILL" +} +``` + +- [ ] **Step 2: 실패 확인** + +Run: `npx bats tests/scripts.bats` → FAIL + +- [ ] **Step 3: Phase 3 서두를 디스패치 루프로 바꾼다** + +`**Phases 3–4 repeat per Wave in `## Waves` order.** A later Wave launches only after the previous Wave is fully approved; `` below = the *current* Wave's task count. Single-Wave splits run everyone in parallel (the original behavior).` 를 다음으로 교체: + +```markdown +**Phase 3–4는 Wave가 아니라 하나의 디스패치 루프다.** 매 회차: + +1. `scripts/ready-set.sh .orchestration/graph.json .orchestration/status ` + → **0** 출력된 id를 디스패치, **2** 던질 것 없고 실행 중인 게 있음(이벤트 대기로), + **3** **교착** — 실패한 의존 또는 사이클. **대기 금지**, 즉시 보고하고 사람 판단을 + 받는다(실행 중인 워커가 없으므로 어떤 이벤트도 오지 않는다), **4** 그래프/status를 + 못 읽음 — 추측 금지, **5** 전 task 종료 → Phase 5로. +2. 디스패치할 각 task에 대해 아래 step 1–3(Orca는 O1–O5)을 수행한다. `` = 이번 + 회차에 디스패치하는 task 수. +3. 이벤트를 기다린다. tmux: `scripts/watch-status.sh --tasks <실행 중인 id들> + impl_done 1` — `--tasks`로 좁히지 않으면 이전 회차의 approved가 + `expected=1`을 즉시 만족시켜 스핀한다. Orca: `scripts/orca-wait.sh`를 그대로 쓴다 + (이미 이벤트 기반이다). +4. 깨어나면 해당 task를 처리(리뷰 → approved 또는 rework)하고 1번으로 돌아간다. + +**brief는 디스패치하는 순간 쓴다.** 그 task가 실제로 consume하는 선행 시그니처만 넣으면 +되고, 그 시점에 선행은 이미 `approved`라 시그니처가 확정돼 있다. +``` + +- [ ] **Step 4: Phase 4의 Wave 문장을 고친다** + +`When this Wave's tasks are all approved, return to Phase 3 step 0 for the next Wave (inject its preceding-interface signatures); once the last Wave is approved, go to Phase 5.` 를 교체: + +```markdown +한 task가 approved되면 디스패치 루프 1번으로 돌아간다 — 그 task가 풀어준 의존이 있으면 +`ready-set.sh`가 다음 회차에 알려주고, 빈 슬롯이 즉시 채워진다. `ready-set.sh`가 **5**를 +반환하면 Phase 5로 간다. +``` + +- [ ] **Step 5: 재진입 절을 고친다** + +`## Re-entry (resume)` 의 첫 문장 뒤에 추가: + +```markdown +Wave 인덱스 같은 중간 상태는 없다. `.orchestration/graph.json`과 `status/*.json`을 읽어 +`ready-set.sh`를 돌리면 그 자체가 복원된 상태다 — 같은 입력이면 같은 답이 나온다. +``` + +- [ ] **Step 6: 테스트 + 전체 스위트** + +Run: `npx bats tests/` +Expected: 실패 0. + +- [ ] **Step 7: 커밋** + +```bash +git add skills/orchestrate/SKILL.md tests/scripts.bats +git -c user.name="Younggi Choi" -c user.email="74581798+choiyounggi@users.noreply.github.com" \ + commit -m "docs(orchestrate): Phase 3+4를 디스패치 루프로, 재진입 단순화 + +Wave 배리어를 없애고 ready-set 판정 → 빈 슬롯 충전 → 이벤트 대기 루프로 바꾼다. +exit 3(교착)은 대기 금지로 명시했다 — 실행 중인 워커가 없으면 이벤트도 오지 않는다." +``` + +--- + +## Self-Review + +**1. 스펙 커버리지** + +| 스펙 절 | 담당 task | +|---|---| +| §3.1 그래프 + 슬롯 회계, deps=approved, 슬롯 점유 범위 | Task 1 | +| §3.2 슬롯 제안 + `LO_MAX_SESSIONS` 상한 | Task 1(집행), Task 3(제안·보고) | +| §3.3 한도/stall 반응 | **기존 동작으로 충족** — `watch-status` exit 7 / `orca-worker-stalled.sh`가 이미 stall을 보고하고, 디스패치 루프는 그때 1번으로 돌아가지 않으므로 큐 투입이 자연히 멈춘다. 새 코드 없음 | +| §3.4 디스패치 루프, brief 주입 시점, `--tasks`, 재진입 | Task 2, Task 4 | +| §4 실패 처리 (exit 0/2/3/4/5, 그래프 검증) | Task 1(전부), Task 3(Phase 2 쓰기 직후 검증 호출) | +| §5 테스트 | Task 1·2의 bats | +| §3.5 분할 | **범위 밖 (PR 2)** — 의도적 | + +§4의 "분할로 인한 변경 시점 검증"은 PR 2 소관이라 여기 없다. 최초 작성 시점 검증은 Task 3 Step 3이 커버한다. + +**2. 플레이스홀더 스캔** — TBD/TODO 없음. 모든 코드 스텝에 실제 코드가 있고, 문서 스텝은 교체할 원문과 새 문장을 모두 적었다. + +**3. 타입/이름 일관성** — `ready-set.sh ` 시그니처가 Task 1 정의, Task 3 Step 3, Task 4 Step 3에서 동일하다. 종료코드 0/2/3/4/5의 의미가 세 곳에서 동일하다. `--tasks `가 Task 2 정의와 Task 4 인용에서 동일하다. `LO_MAX_SESSIONS`는 Task 1(집행)과 Task 3(문서)에서 같은 의미(상한)로 쓰인다. From 486abca9cfd3746a828b08b0a3df4f862481cc7d Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:17:11 +0900 Subject: [PATCH 3/8] =?UTF-8?q?docs(plan):=20SKILL.md=20=EC=82=BD=EC=9E=85?= =?UTF-8?q?=EB=AC=B8=EC=9D=84=20=EC=98=81=EC=96=B4=EB=A1=9C=20=E2=80=94=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=20=EC=B6=A9=EB=8F=8C=20=EC=8A=A4=EC=BA=94=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKILL.md는 한글 0자인 영어 문서인데 계획이 한국어 문단을 넣고 테스트가 한국어를 grep하고 있었다. 배포되는 스킬 본문은 영어가 관례다(guardrails v1.2.0 선례). docs/ 아래 스펙·계획은 한국어 유지. Global Constraints에 규칙으로 명시. --- ...6-08-07-orchestrate-ready-set-scheduler.md | 87 +++++++++++-------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md b/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md index 91085e1..27e0539 100644 --- a/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md +++ b/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md @@ -19,6 +19,7 @@ - 기존 387개 bats는 전부 그린을 유지한다. PR 통과 조건이다. - 분할 경로(스펙 §3.5)는 PR 2다. 이 계획에서 구현하지 않는다. - 커밋 author는 `Younggi Choi <74581798+choiyounggi@users.noreply.github.com>` (공개 레포 관례). +- **`skills/orchestrate/SKILL.md`에 넣는 모든 문장은 영어다.** 현재 이 파일은 한글이 0자이며, 배포되는 스킬 본문의 관례다(guardrails v1.2.0이 배포 메시지를 한국어→영어로 바꾼 것과 같은 이유). `docs/` 아래 스펙·계획 문서만 한국어를 쓴다. SKILL.md를 grep하는 테스트도 영어 문자열을 대상으로 한다. --- @@ -491,7 +492,7 @@ argc는 옵션 파싱 뒤에 잡아 4번째 위치 인자 판정이 깨지지 grep -qF 'LO_MAX_SESSIONS' "$SKILL" # The cap must never be a bare number again: the report has to name what it # protects, or "dynamic" degrades back into a hardcoded 4. - grep -qF '코디네이터 주의력' "$SKILL" + grep -qF 'coordinator attention' "$SKILL" ! grep -qF 'concurrent-session cap** (default 4)' "$SKILL" } ``` @@ -506,8 +507,9 @@ Expected: FAIL — `graph.json`이 SKILL.md에 없음. `## Phase 2 — Decompose`에서 `Apply a **concurrent-session cap** (default 4) — if a Wave exceeds it, split it or ask. Tasks in the same Wave are independent (parallel); a later Wave starts only after the previous Wave is approved.` 문장을 삭제하고, 그 자리에 넣는다: ```markdown -그리고 두 산출물을 **함께** 쓴다: 사람용 `conflict-matrix.md`와 기계용 -`.orchestration/graph.json`. 마크다운 표는 스케줄러가 읽을 수 없다. +Write BOTH artifacts: `conflict-matrix.md` for humans and +`.orchestration/graph.json` for the scheduler. A markdown table is not machine +readable. ```json { "tasks": [ @@ -516,23 +518,27 @@ Expected: FAIL — `graph.json`이 SKILL.md에 없음. ] } ``` -Wave는 **실행 단위가 아니라 Gate 1 보고서의 예시**다. 실행은 `ready-set.sh`가 -판정한다: 의존이 `approved` 이상인 task를 빈 슬롯만큼 흘려보낸다. 위상정렬은 여전히 -하되, 그 결과는 "이런 순서로 흐를 겁니다"를 보여주기 위한 것이다. - -**슬롯 수를 제안한다.** task 수·크기·위험도를 근거로 숫자를 정하고, **무엇을 보호하는 -값인지 함께 밝힌다** — 이 캡이 지키는 것은 머신 자원이 아니라 **코디네이터 주의력**과 -**API 사용량/예산**이다(둘 다 OS에서 조회할 수 없어 판단으로 정한다). 슬롯은 디스패치된 -순간부터 종료 상태까지 점유되며, `plan_ready`/`impl_done` 같은 리뷰 대기도 점유로 센다 — -리뷰 안 된 task가 쌓인 채 새 task가 들어오면 캡이 무의미해지기 때문이다. -`LO_MAX_SESSIONS`가 설정돼 있으면 그 값이 상한이며 제안을 덮어쓴다. +Waves are **an illustration in the Gate 1 report, not an execution unit.** +Execution is decided by `ready-set.sh`: a task runs as soon as its dependencies +are `approved` and a slot is free. Still topologically sort — the result shows +the user the expected flow — but nothing waits on a Wave boundary. + +**Propose the slot count.** Pick the number from the task count, their size, and +their risk, and **say what the number protects**: this cap guards **coordinator +attention** and **API usage/budget**, not machine resources. Neither is +queryable, which is why it is a judgement rather than a computation. A slot is +held from dispatch until the task reaches a terminal state — `plan_ready` and +`impl_done` (review pending) count as held, because a pile of unreviewed tasks +next to a stream of new ones makes the cap meaningless. When `LO_MAX_SESSIONS` +is set it is an upper bound and overrides the proposal. ``` `graph.json`을 쓴 직후 검증한다는 문장을 덧붙인다: ```markdown -쓴 뒤 `scripts/ready-set.sh `를 한 번 돌려 **exit 4가 아닌지** -확인한다 — 깨진 JSON·`.tasks` 누락·정의되지 않은 의존을 여기서 잡는다. +After writing it, run `scripts/ready-set.sh ` once and +confirm it does **not** exit 4 — malformed JSON, a missing `.tasks` array, and a +dependency naming an undefined task are all caught here. ``` - [ ] **Step 4: Gate 1 보고 항목을 고친다** @@ -540,8 +546,9 @@ Wave는 **실행 단위가 아니라 Gate 1 보고서의 예시**다. 실행은 `## 🚦 Gate 1` 의 `Report the task list, Waves, session count, and a rough cost note.` 를 다음으로 교체: ```markdown -Report the task list, the dependency graph (예상 흐름을 Wave 형태로 보여줘도 좋다), the -proposed **slot count with its rationale and what it protects**, and a rough cost note. +Report the task list, the dependency graph (showing the expected flow as Waves is +fine), the proposed **slot count with its rationale and what it protects**, and a +rough cost note. ``` - [ ] **Step 5: 테스트 통과 확인** @@ -581,7 +588,7 @@ git -c user.name="Younggi Choi" -c user.email="74581798+choiyounggi@users.norepl grep -qF '--tasks' "$SKILL" # exit 3 is the guard this design turns on; it must be spelled out as # "do not wait", or it degrades into the silent stall it exists to prevent. - grep -qF '교착' "$SKILL" + grep -qF 'DEADLOCK' "$SKILL" # Waves must no longer be described as an execution barrier. ! grep -qF 'A later Wave launches only after' "$SKILL" } @@ -596,23 +603,26 @@ Run: `npx bats tests/scripts.bats` → FAIL `**Phases 3–4 repeat per Wave in `## Waves` order.** A later Wave launches only after the previous Wave is fully approved; `` below = the *current* Wave's task count. Single-Wave splits run everyone in parallel (the original behavior).` 를 다음으로 교체: ```markdown -**Phase 3–4는 Wave가 아니라 하나의 디스패치 루프다.** 매 회차: +**Phases 3–4 are one dispatch loop, not a per-Wave repeat.** Each round: 1. `scripts/ready-set.sh .orchestration/graph.json .orchestration/status ` - → **0** 출력된 id를 디스패치, **2** 던질 것 없고 실행 중인 게 있음(이벤트 대기로), - **3** **교착** — 실패한 의존 또는 사이클. **대기 금지**, 즉시 보고하고 사람 판단을 - 받는다(실행 중인 워커가 없으므로 어떤 이벤트도 오지 않는다), **4** 그래프/status를 - 못 읽음 — 추측 금지, **5** 전 task 종료 → Phase 5로. -2. 디스패치할 각 task에 대해 아래 step 1–3(Orca는 O1–O5)을 수행한다. `` = 이번 - 회차에 디스패치하는 task 수. -3. 이벤트를 기다린다. tmux: `scripts/watch-status.sh --tasks <실행 중인 id들> - impl_done 1` — `--tasks`로 좁히지 않으면 이전 회차의 approved가 - `expected=1`을 즉시 만족시켜 스핀한다. Orca: `scripts/orca-wait.sh`를 그대로 쓴다 - (이미 이벤트 기반이다). -4. 깨어나면 해당 task를 처리(리뷰 → approved 또는 rework)하고 1번으로 돌아간다. - -**brief는 디스패치하는 순간 쓴다.** 그 task가 실제로 consume하는 선행 시그니처만 넣으면 -되고, 그 시점에 선행은 이미 `approved`라 시그니처가 확정돼 있다. + → **0** dispatch the printed ids, **2** nothing dispatchable but work is in + flight (go wait for an event), **3** **DEADLOCK** — a failed dependency or a + cycle: **do not wait**, report it and get a human decision (with no worker + running, no event can ever arrive), **4** the graph or status could not be + read — refuse, do not guess, **5** every task is terminal → go to Phase 5. +2. For each dispatched task run steps 1–3 below (O1–O5 on Orca). `` = the + number of tasks dispatched in THIS round. +3. Wait for an event. tmux: `scripts/watch-status.sh --tasks + impl_done 1` — without `--tasks` the tasks approved in earlier + rounds satisfy `expected=1` immediately and the wait spins. Orca: + `scripts/orca-wait.sh` unchanged; it is already event-driven. +4. On wake, handle that task (review → approved, or inject rework), then return + to step 1. + +**Write each brief at dispatch time.** It only needs the preceding signatures +that this task actually consumes, and by then those tasks are `approved`, so the +signatures are settled. ``` - [ ] **Step 4: Phase 4의 Wave 문장을 고친다** @@ -620,9 +630,9 @@ Run: `npx bats tests/scripts.bats` → FAIL `When this Wave's tasks are all approved, return to Phase 3 step 0 for the next Wave (inject its preceding-interface signatures); once the last Wave is approved, go to Phase 5.` 를 교체: ```markdown -한 task가 approved되면 디스패치 루프 1번으로 돌아간다 — 그 task가 풀어준 의존이 있으면 -`ready-set.sh`가 다음 회차에 알려주고, 빈 슬롯이 즉시 채워진다. `ready-set.sh`가 **5**를 -반환하면 Phase 5로 간다. +When a task is approved, return to step 1 of the dispatch loop — whatever +dependency it released shows up in the next `ready-set.sh` round and the freed +slot is refilled immediately. When `ready-set.sh` returns **5**, go to Phase 5. ``` - [ ] **Step 5: 재진입 절을 고친다** @@ -630,8 +640,9 @@ Run: `npx bats tests/scripts.bats` → FAIL `## Re-entry (resume)` 의 첫 문장 뒤에 추가: ```markdown -Wave 인덱스 같은 중간 상태는 없다. `.orchestration/graph.json`과 `status/*.json`을 읽어 -`ready-set.sh`를 돌리면 그 자체가 복원된 상태다 — 같은 입력이면 같은 답이 나온다. +There is no intermediate state such as a Wave index to restore. Reading +`.orchestration/graph.json` plus `status/*.json` and running `ready-set.sh` IS +the restored state — the same inputs always yield the same answer. ``` - [ ] **Step 6: 테스트 + 전체 스위트** From 4382d5f2492b1416d9d51b5a7f8d0ea1abbcd57b Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:25:45 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat(orchestrate):=20ready-set.sh=20?= =?UTF-8?q?=E2=80=94=20=EC=9D=98=EC=A1=B4=20=EA=B7=B8=EB=9E=98=ED=94=84=20?= =?UTF-8?q?+=20=EC=8A=AC=EB=A1=AF=20=ED=9A=8C=EA=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EB=94=94=EC=8A=A4=ED=8C=A8=EC=B9=98=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=EC=A7=91=ED=95=A9=20=ED=8C=90=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 배리어 교체의 판정 절반. deps 충족은 approved 이상에서만 성립하고, 리뷰 대기(plan_ready/impl_done)도 슬롯을 점유한다. 실패한 의존으로 인한 교착은 exit 3으로 즉시 드러나며 조용한 대기(exit 2)와 구분된다. --- skills/orchestrate/scripts/ready-set.sh | 118 ++++++++++++++++++++++++ tests/ready-set.bats | 113 +++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100755 skills/orchestrate/scripts/ready-set.sh create mode 100644 tests/ready-set.bats diff --git a/skills/orchestrate/scripts/ready-set.sh b/skills/orchestrate/scripts/ready-set.sh new file mode 100755 index 0000000..e3d5a7d --- /dev/null +++ b/skills/orchestrate/scripts/ready-set.sh @@ -0,0 +1,118 @@ +#!/bin/sh +# ready-set.sh — which tasks may be dispatched right now? +# +# This is the Wave-barrier replacement. Waves made the whole batch wait for its +# slowest member; here a task is dispatchable the moment its own dependencies +# are approved and a slot is free, so a finished worker is refilled immediately. +# +# usage: ready-set.sh +# {"tasks":[{"id":"t1","deps":["t0"]}, ...]} — Phase 2 writes it. +# Only `id` and `deps` are read here; files/outputs/consumes are +# the coordinator's conflict-matrix fields. +# .orchestration/status — one .json per task, `.phase` +# written by status-update.sh. A task with no file is `pending`. +# the coordinator's approved slot count. LO_MAX_SESSIONS, when +# set, is an UPPER BOUND on it, never a raise. +# +# exit 0 dispatch these (stdout: one task id per line, at most of them) +# exit 2 nothing to dispatch, but tasks are in flight — wait for an event +# exit 3 nothing to dispatch, nothing in flight, unfinished tasks remain — +# DEADLOCK (a failed dependency, or a cycle). Never wait on this: with +# no worker running, no event can ever arrive. Report it. +# exit 4 the graph or the status dir could not be read, or an argument is +# invalid — refuse rather than guess +# exit 5 every task is in a terminal state — the run is complete +# exit 127 jq not found +# +# A dependency counts as satisfied only at `approved` or higher, NOT at +# impl_done: a task that consumes an unreviewed interface has to be redone when +# rework changes that signature. This is the Wave model's "previous Wave fully +# approved" guarantee, narrowed from a global barrier to a per-task wait. +# +# env: +# LO_MAX_SESSIONS upper bound on (a tuning knob like LO_PHASE_TIMEOUTS) +set -u + +JQ=$(command -v jq) || { echo "ready-set: jq not found" >&2; exit 127; } + +graph="${1:-}"; sdir="${2:-}"; cap="${3:-}" +[ -n "$graph" ] && [ -n "$sdir" ] && [ -n "$cap" ] || { + echo "usage: ready-set.sh " >&2; exit 4; } +[ -f "$graph" ] || { echo "ready-set: graph '$graph' not found" >&2; exit 4; } +[ -d "$sdir" ] || { echo "ready-set: status dir '$sdir' not found" >&2; exit 4; } + +case "$cap" in ''|*[!0-9]*) echo "ready-set: cap must be a positive integer" >&2; exit 4 ;; esac +[ "$cap" -gt 0 ] || { echo "ready-set: cap must be > 0" >&2; exit 4; } + +# LO_MAX_SESSIONS caps the cap. It is a ceiling the operator sets, so it lowers +# the coordinator's proposal and never raises it. +if [ -n "${LO_MAX_SESSIONS:-}" ]; then + case "$LO_MAX_SESSIONS" in + ''|*[!0-9]*) echo "ready-set: LO_MAX_SESSIONS must be a positive integer" >&2; exit 4 ;; + esac + [ "$LO_MAX_SESSIONS" -gt 0 ] || { echo "ready-set: LO_MAX_SESSIONS must be > 0" >&2; exit 4; } + [ "$LO_MAX_SESSIONS" -lt "$cap" ] && cap="$LO_MAX_SESSIONS" +fi + +ids=$("$JQ" -r '.tasks[]?.id // empty' "$graph" 2>/dev/null) || { + echo "ready-set: graph '$graph' is not valid JSON" >&2; exit 4; } + +# `.tasks` absent is a malformed graph, not an empty one — tell them apart so a +# typo'd key cannot read as "nothing to do". +"$JQ" -e 'has("tasks") and (.tasks | type == "array")' "$graph" >/dev/null 2>&1 || { + echo "ready-set: graph '$graph' has no .tasks array" >&2; exit 4; } + +phase_of() { # $1 = task id -> its recorded phase, or "pending" when unrecorded + f="$sdir/$1.json" + [ -f "$f" ] || { echo pending; return; } + p=$("$JQ" -r '.phase // "pending"' "$f" 2>/dev/null) || p=pending + [ -n "$p" ] || p=pending + echo "$p" +} + +is_satisfied() { # $1 = phase -> 0 when a dependent may start on it + case "$1" in approved|merged|done) return 0 ;; *) return 1 ;; esac +} +is_terminal() { # $1 = phase -> 0 when the task will never occupy a slot again + case "$1" in approved|merged|done|failed) return 0 ;; *) return 1 ;; esac +} + +busy=0; unfinished=0; ready="" +for id in $ids; do + ph=$(phase_of "$id") + if is_terminal "$ph"; then + # `failed` is terminal for scheduling but is NOT completion: it leaves its + # dependents permanently unreachable, which is what exit 3 exists to report. + [ "$ph" = failed ] && unfinished=$((unfinished + 1)) + continue + fi + unfinished=$((unfinished + 1)) + if [ "$ph" != pending ]; then busy=$((busy + 1)); continue; fi + + deps=$("$JQ" -r --arg id "$id" '.tasks[] | select(.id == $id) | .deps[]? // empty' "$graph" 2>/dev/null) + ok=1 + for d in $deps; do + # A dep naming a task the graph does not define is a malformed graph, not an + # unsatisfied edge — refuse instead of silently blocking that task forever. + echo "$ids" | grep -qx "$d" || { echo "ready-set: task '$id' depends on unknown '$d'" >&2; exit 4; } + is_satisfied "$(phase_of "$d")" || { ok=0; break; } + done + [ "$ok" = 1 ] && ready="$ready $id" +done + +[ "$unfinished" -eq 0 ] && { echo "[ready-set] all tasks terminal"; exit 5; } + +free=$((cap - busy)) +[ "$free" -lt 0 ] && free=0 + +n=0 +for id in $ready; do + [ "$n" -ge "$free" ] && break + echo "$id"; n=$((n + 1)) +done +[ "$n" -gt 0 ] && exit 0 + +[ "$busy" -gt 0 ] && { echo "[ready-set] nothing dispatchable, $busy in flight — wait" >&2; exit 2; } + +echo "[ready-set] DEADLOCK: $unfinished task(s) unfinished, none dispatchable, none running — a failed dependency or a cycle. Inspect the graph and status; do not wait." >&2 +exit 3 diff --git a/tests/ready-set.bats b/tests/ready-set.bats new file mode 100644 index 0000000..5163391 --- /dev/null +++ b/tests/ready-set.bats @@ -0,0 +1,113 @@ +#!/usr/bin/env bats +# Tests for ready-set.sh — the Wave-barrier replacement. Given the dependency +# graph and each task's recorded phase, it answers exactly one question: +# which tasks may be dispatched right now. It never launches anything. + +setup() { + RS="${BATS_TEST_DIRNAME}/../skills/orchestrate/scripts/ready-set.sh" + G="${BATS_TEST_TMPDIR}/graph.json" + S="${BATS_TEST_TMPDIR}/status" + mkdir -p "$S" +} + +graph() { printf '%s' "$1" > "$G"; } +phase() { printf '{"task":"%s","phase":"%s"}' "$1" "$2" > "$S/$1.json"; } + +@test "no deps, empty status: every task is dispatchable up to the cap" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":[]},{"id":"t3","deps":[]}]}' + run sh "$RS" "$G" "$S" 2 + [ "$status" -eq 0 ] + [ "$(echo "$output" | grep -c .)" -eq 2 ] +} + +@test "a dependent waits until its dep is approved, not merely impl_done" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":["t1"]}]}' + phase t1 impl_done + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 2 ] # t1 busy (review pending), t2 not yet startable + + phase t1 approved + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 0 ] + [ "$output" = "t2" ] +} + +@test "review-pending phases occupy a slot (plan_ready and impl_done both count)" { + graph '{"tasks":[{"id":"a","deps":[]},{"id":"b","deps":[]},{"id":"c","deps":[]}]}' + phase a plan_ready + phase b impl_done + run sh "$RS" "$G" "$S" 2 + [ "$status" -eq 2 ] # cap 2 fully occupied by two review-waiting tasks +} + +@test "a failed dependency is a deadlock, never a quiet wait (the core guard)" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":["t1"]}]}' + phase t1 failed + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 3 ] + [[ "$output" == *"DEADLOCK"* ]] +} + +@test "a cycle surfaces as the same deadlock (boundary)" { + graph '{"tasks":[{"id":"t1","deps":["t2"]},{"id":"t2","deps":["t1"]}]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 3 ] +} + +@test "all tasks terminal: exit 5, the run is complete (boundary)" { + graph '{"tasks":[{"id":"t1","deps":[]},{"id":"t2","deps":["t1"]}]}' + phase t1 approved + phase t2 merged + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 5 ] +} + +@test "an empty task array is complete, not a deadlock (boundary)" { + graph '{"tasks":[]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 5 ] +} + +@test "a graph with no .tasks array is refused, not read as empty (error)" { + graph '{"nodes":[]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 4 ] +} + +@test "malformed JSON is refused (error)" { + graph '}{ not json' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 4 ] +} + +@test "a dep naming an undefined task is refused, not blocked forever (error)" { + graph '{"tasks":[{"id":"t1","deps":["ghost"]}]}' + run sh "$RS" "$G" "$S" 4 + [ "$status" -eq 4 ] +} + +@test "LO_MAX_SESSIONS lowers the cap but never raises it" { + graph '{"tasks":[{"id":"a","deps":[]},{"id":"b","deps":[]},{"id":"c","deps":[]}]}' + run env LO_MAX_SESSIONS=1 sh "$RS" "$G" "$S" 3 + [ "$status" -eq 0 ] + [ "$(echo "$output" | grep -c .)" -eq 1 ] + + run env LO_MAX_SESSIONS=9 sh "$RS" "$G" "$S" 2 + [ "$(echo "$output" | grep -c .)" -eq 2 ] +} + +@test "a non-numeric or zero cap is refused (boundary)" { + graph '{"tasks":[{"id":"a","deps":[]}]}' + run sh "$RS" "$G" "$S" 0 + [ "$status" -eq 4 ] + run sh "$RS" "$G" "$S" abc + [ "$status" -eq 4 ] +} + +@test "a missing graph or status dir is refused (error)" { + run sh "$RS" "$BATS_TEST_TMPDIR/nope.json" "$S" 2 + [ "$status" -eq 4 ] + graph '{"tasks":[]}' + run sh "$RS" "$G" "$BATS_TEST_TMPDIR/nodir" 2 + [ "$status" -eq 4 ] +} From 3bc4c01e98f3dee08510b205d93397b1a896fe78 Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:42:29 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat(orchestrate):=20watch-status=20--tasks?= =?UTF-8?q?=20=E2=80=94=20=EC=B6=94=EC=A0=81=20=EC=A4=91=EC=9D=B8=20?= =?UTF-8?q?=EC=A7=91=ED=95=A9=EC=9C=BC=EB=A1=9C=20=EC=8A=A4=EC=BA=94=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 슬롯 스케줄러는 '지금 돌리는 것 중 아무나 하나 도달'이 필요한데, status 디렉토리 전체를 세면 이전 라운드의 approved가 expected=1을 즉시 만족시켜 스핀한다. argc는 옵션 파싱 뒤에 잡아 4번째 위치 인자 판정이 깨지지 않게 한다. --- skills/orchestrate/scripts/watch-status.sh | 26 ++++++++++++- tests/scripts.bats | 45 ++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/skills/orchestrate/scripts/watch-status.sh b/skills/orchestrate/scripts/watch-status.sh index a3c5e77..9897a0f 100755 --- a/skills/orchestrate/scripts/watch-status.sh +++ b/skills/orchestrate/scripts/watch-status.sh @@ -3,7 +3,11 @@ # phase, then exit 0. The orchestrator launches this with run_in_background; on # exit the harness re-invokes the orchestrator. # -# usage: watch-status.sh [timeout-sec] [interval-sec] +# usage: watch-status.sh [--tasks ] [timeout-sec] [interval-sec] +# --tasks: scope the scan to only the given comma-separated task ids. The slot +# scheduler needs to wake as soon as ANY currently-running task reaches the target; +# without scoping, the full status dir would count tasks approved in earlier rounds +# and the wait would spin on stale data. # exit 0: all reached target (or higher) # exit 2: timeout # exit 3: a failed session detected (abort → orchestrator intervenes) @@ -32,6 +36,20 @@ set -eu JQ=$(command -v jq) || { echo "watch-status: jq not found" >&2; exit 127; } +# --tasks scopes the scan to the given ids. The slot scheduler needs "any ONE of +# the tasks I am currently running reached the target"; counting the whole status +# dir would satisfy expected=1 from tasks approved in earlier rounds and spin. +only="" +while [ $# -gt 0 ]; do + case "$1" in + --tasks) shift; only="${1:-}"; [ -n "$only" ] || { echo "watch-status: --tasks needs a comma-separated id list" >&2; exit 4; }; shift ;; + --) shift; break ;; + -*) echo "watch-status: unknown option '$1'" >&2; exit 4 ;; + *) break ;; + esac +done +# Captured AFTER option parsing: argc decides whether the 4th POSITIONAL was +# given, and options must not be counted toward it. argc=$# dir="$1"; target="$2"; expected="$3"; timeout="${4:-3600}"; interval="${5:-15}" @@ -128,6 +146,12 @@ while [ "$elapsed" -lt "$budget" ]; do done_count=0; failed=0; summary=""; stalled="" for f in "$dir"/*.json; do [ -f "$f" ] || continue + if [ -n "$only" ]; then + base=${f##*/}; base=${base%.json} + # Exact membership on a comma-delimited list: the commas around both sides + # keep `t1` from matching `t12`. + case ",$only," in *",$base,"*) : ;; *) continue ;; esac + fi ph=$("$JQ" -r '.phase // "pending"' "$f" 2>/dev/null || echo "pending") tk=$("$JQ" -r '.task // "?"' "$f" 2>/dev/null || echo "?") summary="$summary $tk:$ph" diff --git a/tests/scripts.bats b/tests/scripts.bats index 1f08fd3..6612ca5 100644 --- a/tests/scripts.bats +++ b/tests/scripts.bats @@ -54,3 +54,48 @@ setup() { [ "$(jq -r 'has("notakeyvalue")' "$STATUS_DIR/task-x.json")" = "false" ] [ "$(jq -r '.worktree | type' "$STATUS_DIR/task-x.json")" = "string" ] } + +@test "watch-status --tasks: only the named tasks are counted" { + mkdir -p "$STATUS_DIR" + printf '{"task":"old","phase":"approved"}' > "$STATUS_DIR/old.json" + printf '{"task":"a","phase":"implementing"}' > "$STATUS_DIR/a.json" + # `old` already passed the target, but it is not being tracked: without + # scoping, expected=1 would be satisfied instantly and the wait would spin. + run bash "$WS" --tasks a "$STATUS_DIR" impl_done 1 2 1 + [ "$status" -eq 2 ] +} + +@test "watch-status --tasks: returns as soon as ANY tracked task reaches target" { + mkdir -p "$STATUS_DIR" + printf '{"task":"a","phase":"impl_done"}' > "$STATUS_DIR/a.json" + printf '{"task":"b","phase":"implementing"}' > "$STATUS_DIR/b.json" + run bash "$WS" --tasks a,b "$STATUS_DIR" impl_done 1 5 1 + [ "$status" -eq 0 ] +} + +@test "watch-status --tasks: an untracked failed task does not abort the wait" { + mkdir -p "$STATUS_DIR" + printf '{"task":"old","phase":"failed"}' > "$STATUS_DIR/old.json" + printf '{"task":"a","phase":"implementing"}' > "$STATUS_DIR/a.json" + run bash "$WS" --tasks a "$STATUS_DIR" impl_done 1 2 1 + [ "$status" -eq 2 ] +} + +@test "watch-status --tasks: an explicit timeout argument still outranks the env" { + # --tasks is consumed before the positional count is taken; if argc were + # captured before that, the 4th positional would stop being recognised and + # LO_PHASE_TIMEOUTS would silently win. + mkdir -p "$STATUS_DIR" + printf '{"task":"a","phase":"implementing"}' > "$STATUS_DIR/a.json" + run env LO_PHASE_TIMEOUTS="impl_done=999" bash "$WS" --tasks a "$STATUS_DIR" impl_done 1 2 1 + [[ "$output" == *"budget=2s"* ]] + [[ "$output" == *"source=arg"* ]] +} + +@test "watch-status: without --tasks every status file is still counted (no regression)" { + mkdir -p "$STATUS_DIR" + printf '{"task":"a","phase":"done"}' > "$STATUS_DIR/a.json" + printf '{"task":"b","phase":"done"}' > "$STATUS_DIR/b.json" + run bash "$WS" "$STATUS_DIR" impl_done 2 5 1 + [ "$status" -eq 0 ] +} From 8534d86a7ff6695886033f560fe5dbcf8c00d182 Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:57:00 +0900 Subject: [PATCH 6/8] =?UTF-8?q?docs(orchestrate):=20Phase=202=20=E2=80=94?= =?UTF-8?q?=20graph.json=20=EC=82=B0=EC=B6=9C=20+=20=EA=B7=BC=EA=B1=B0=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=8A=AC=EB=A1=AF=20=EC=A0=9C=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 하드코딩 4를 없애고, 캡이 보호하는 자원(코디네이터 주의력/API 예산)을 보고에 명시하게 한다. LO_MAX_SESSIONS가 상한. --- skills/orchestrate/SKILL.md | 34 ++++++++++++++++++++++++++++++---- tests/scripts.bats | 10 ++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/skills/orchestrate/SKILL.md b/skills/orchestrate/SKILL.md index 0c41a27..2e6e34a 100644 --- a/skills/orchestrate/SKILL.md +++ b/skills/orchestrate/SKILL.md @@ -81,9 +81,33 @@ creates — component/schema/endpoint/type), and **consumes** (another task's ou it depends on). Build a conflict/dependency matrix from those and topologically sort into Waves (`conflict-matrix.md`): a dependency edge `A → B` means B consumes A's output, so A's Wave precedes B's. Detect duplicate outputs and assign a single -producer; others consume (add a dependency edge). Apply a **concurrent-session cap** -(default 4) — if a Wave exceeds it, split it or ask. Tasks in the same Wave are -independent (parallel); a later Wave starts only after the previous Wave is approved. +producer; others consume (add a dependency edge). Write BOTH artifacts: `conflict-matrix.md` for humans and +`.orchestration/graph.json` for the scheduler. A markdown table is not machine +readable. + +```json +{ "tasks": [ + { "id": "t1", "deps": [], "files": ["src/auth/**"], "outputs": ["AuthToken"] }, + { "id": "t3", "deps": ["t1"], "files": ["src/api/**"], "consumes": ["AuthToken"] } +] } +``` + +Waves are **an illustration in the Gate 1 report, not an execution unit.** +Execution is decided by `ready-set.sh`: a task runs as soon as its dependencies +are `approved` and a slot is free. Still topologically sort — the result shows +the user the expected flow — but nothing waits on a Wave boundary. + +**Propose the slot count.** Pick the number from the task count, their size, and +their risk, and **say what the number protects**: this cap guards **coordinator attention** and **API usage/budget**, not machine resources. Neither is +queryable, which is why it is a judgement rather than a computation. A slot is +held from dispatch until the task reaches a terminal state — `plan_ready` and +`impl_done` (review pending) count as held, because a pile of unreviewed tasks +next to a stream of new ones makes the cap meaningless. When `LO_MAX_SESSIONS` +is set it is an upper bound and overrides the proposal. + +After writing it, run `scripts/ready-set.sh ` once and +confirm it does **not** exit 4 — malformed JSON, a missing `.tasks` array, and a +dependency naming an undefined task are all caught here. **Visual spec (`design` role).** While extracting the above, flag each task that is UI-facing *and* whose source issue references a design (e.g. a Figma link). If the @@ -93,7 +117,9 @@ tasks, or any task with no design reference, skip this. With `design` unset, ign design links entirely — the original behavior. ## 🚦 Gate 1 — task-split approval (REQUIRED) -Report the task list, Waves, session count, and a rough cost note. **Wait for the +Report the task list, the dependency graph (showing the expected flow as Waves is +fine), the proposed **slot count with its rationale and what it protects**, and a +rough cost note. **Wait for the user's approval** before launching anything. **Substrate — ask here, in this same turn.** Before writing that report, run diff --git a/tests/scripts.bats b/tests/scripts.bats index 6612ca5..ab0b8dc 100644 --- a/tests/scripts.bats +++ b/tests/scripts.bats @@ -99,3 +99,13 @@ setup() { run bash "$WS" "$STATUS_DIR" impl_done 2 5 1 [ "$status" -eq 0 ] } + +@test "SKILL.md Phase 2: graph.json artifact and the slot proposal are documented" { + SKILL="${BATS_TEST_DIRNAME}/../skills/orchestrate/SKILL.md" + grep -qF '.orchestration/graph.json' "$SKILL" + grep -qF 'LO_MAX_SESSIONS' "$SKILL" + # The cap must never be a bare number again: the report has to name what it + # protects, or "dynamic" degrades back into a hardcoded 4. + grep -qF 'coordinator attention' "$SKILL" + ! grep -qF 'concurrent-session cap** (default 4)' "$SKILL" +} From b4b4b354f9e2f79d7bdb343918720a2089412126 Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:10:55 +0900 Subject: [PATCH 7/8] =?UTF-8?q?docs(orchestrate):=20Phase=203+4=EB=A5=BC?= =?UTF-8?q?=20=EB=94=94=EC=8A=A4=ED=8C=A8=EC=B9=98=20=EB=A3=A8=ED=94=84?= =?UTF-8?q?=EB=A1=9C,=20=EC=9E=AC=EC=A7=84=EC=9E=85=20=EB=8B=A8=EC=88=9C?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 배리어를 없애고 ready-set 판정 → 빈 슬롯 충전 → 이벤트 대기 루프로 바꾼다. exit 3(교착)은 대기 금지로 명시했다 — 실행 중인 워커가 없으면 이벤트도 오지 않는다. - Phase 3: 디스패치 루프로 재구성 (ready-set + watch-status --tasks) - Phase 4: Wave-return 문장을 dispatch-loop 복귀로 변경 - Re-entry: 중간 상태(Wave index) 불필요 명시 --- skills/orchestrate/SKILL.md | 35 +++++++++++++++++++++++++++-------- tests/scripts.bats | 11 +++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/skills/orchestrate/SKILL.md b/skills/orchestrate/SKILL.md index 2e6e34a..6f64670 100644 --- a/skills/orchestrate/SKILL.md +++ b/skills/orchestrate/SKILL.md @@ -132,10 +132,27 @@ detected Orca always asks — there is no default, no remembered choice, no environment override. Launch nothing until both the split and the substrate are answered. -## Phase 3 — Launch + plan (per Wave) -**Phases 3–4 repeat per Wave in `## Waves` order.** A later Wave launches only after -the previous Wave is fully approved; `` below = the *current* Wave's task count. -Single-Wave splits run everyone in parallel (the original behavior). +## Phase 3 — Launch + plan (dispatch loop) +**Phases 3–4 are one dispatch loop, not a per-Wave repeat.** Each round: + +1. `scripts/ready-set.sh .orchestration/graph.json .orchestration/status ` + → **0** dispatch the printed ids, **2** nothing dispatchable but work is in + flight (go wait for an event), **3** **DEADLOCK** — a failed dependency or a + cycle: **do not wait**, report it and get a human decision (with no worker + running, no event can ever arrive), **4** the graph or status could not be + read — refuse, do not guess, **5** every task is in a terminal state → go to Phase 5. +2. For each dispatched task run steps 1–3 below (O1–O5 on Orca). `` = the + number of tasks dispatched in THIS round. +3. Wait for an event. tmux: `scripts/watch-status.sh --tasks + impl_done 1` — without `--tasks` the tasks approved in earlier + rounds satisfy `expected=1` immediately and the wait spins. Orca: + `scripts/orca-wait.sh` unchanged; it is already event-driven. +4. On wake, handle that task (review → approved, or inject rework), then return + to step 1. + +**Write each brief at dispatch time.** It only needs the preceding signatures +that this task actually consumes, and by then those tasks are `approved`, so the +signatures are settled. **Session knobs (tmux substrate, set once per run):** `export LO_RUN_ID=` so every `launch-session.sh` gets a collision-proof name `lo--` (reuse that @@ -370,9 +387,9 @@ wait with `scripts/orca-wait.sh`. Rework rounds are further Tasks on the same worktree diff (`git -C diff ...HEAD`); if a session's tests look weak, **cross-call `test-quality-auditor` yourself** (self-call + orchestrator cross-call). On shortfall, write `reviews/-rN.md`, inject §3 (rework), repeat. After 3 -failed rounds, escalate. When this Wave's tasks are all approved, return to Phase 3 -step 0 for the next Wave (inject its preceding-interface signatures); once the last -Wave is approved, go to Phase 5. +failed rounds, escalate. When a task is approved, return to step 1 of the dispatch +loop — whatever dependency it released shows up in the next `ready-set.sh` round and +the freed slot is refilled immediately. When `ready-set.sh` returns **5**, go to Phase 5. ## Phase 5 — Integration test loop (max 3) Merge-preview onto the integration branch and run the integration tests (use the @@ -405,7 +422,9 @@ worktree) still fire — a dry run never looks safer than the real one. On re-invocation with no context, measure real state first: `git worktree list`, each `.orchestration/status/*.json` phase, and which `briefs/plans/reviews/` artifacts exist. Resume from the earliest incomplete step (idempotently skip done -steps). Check `tmux ls`, and run `scripts/tmux-worker-stalled.sh ` on each +steps). There is no intermediate state such as a Wave index to restore. Reading +`.orchestration/graph.json` plus `status/*.json` and running `ready-set.sh` IS +the restored state — the same inputs always yield the same answer. Check `tmux ls`, and run `scripts/tmux-worker-stalled.sh ` on each live one — a session that exists is not a worker that moves. Relaunch dead sessions and re-deliver the right prompt with `scripts/send-prompt.sh send`. For leftovers of a run that already died, `scripts/safe-cleanup.sh list-orphans ` enumerates them diff --git a/tests/scripts.bats b/tests/scripts.bats index ab0b8dc..61b59dd 100644 --- a/tests/scripts.bats +++ b/tests/scripts.bats @@ -109,3 +109,14 @@ setup() { grep -qF 'coordinator attention' "$SKILL" ! grep -qF 'concurrent-session cap** (default 4)' "$SKILL" } + +@test "SKILL.md: the dispatch loop cites ready-set exit codes and --tasks" { + SKILL="${BATS_TEST_DIRNAME}/../skills/orchestrate/SKILL.md" + grep -qF 'scripts/ready-set.sh' "$SKILL" + grep -q -- '--tasks' "$SKILL" + # exit 3 is the guard this design turns on; it must be spelled out as + # "do not wait", or it degrades into the silent stall it exists to prevent. + grep -qF 'DEADLOCK' "$SKILL" + # Waves must no longer be described as an execution barrier. + ! grep -qF 'A later Wave launches only after' "$SKILL" +} From 1212bdcf34d11b5bdfa3c1abda7e3258fb8aea4f Mon Sep 17 00:00:00 2001 From: Younggi Choi <74581798+choiyounggi@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:40:53 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix(orchestrate):=20Phase=203+4=20dispatch?= =?UTF-8?q?=20loop=20=E2=80=94=20add=20implement=20delivery=20+=20error=20?= =?UTF-8?q?handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FINDING 1: loop now includes step 3 to deliver implement prompt (was missing, causing hang at plan_ready). Both tmux (send-prompt.sh send) and Orca (task-create implement Task) paths documented. FINDING 2: exit codes 3 (DEADLOCK) and 4 (bad read) now explicitly return to step 1 after human intervention or fix; loop does not stop on errors. FINDING 3: strengthen contract test to verify loop structure (step 1=ready-set, step 3=deliver, step 4=watch --tasks, step 5=return to 1), not just keywords. Plan doc updated to match integrated Phase 3+4 with 5-step loop structure. --- ...6-08-07-orchestrate-ready-set-scheduler.md | 91 +++++++++++++------ skills/orchestrate/SKILL.md | 45 ++++++--- tests/scripts.bats | 21 ++++- 3 files changed, 110 insertions(+), 47 deletions(-) diff --git a/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md b/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md index 27e0539..7d95891 100644 --- a/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md +++ b/docs/superpowers/plans/2026-08-07-orchestrate-ready-set-scheduler.md @@ -598,9 +598,9 @@ git -c user.name="Younggi Choi" -c user.email="74581798+choiyounggi@users.norepl Run: `npx bats tests/scripts.bats` → FAIL -- [ ] **Step 3: Phase 3 서두를 디스패치 루프로 바꾼다** +- [ ] **Step 3: Phase 3+4을 하나의 디스패치 루프로 통합한다** -`**Phases 3–4 repeat per Wave in `## Waves` order.** A later Wave launches only after the previous Wave is fully approved; `` below = the *current* Wave's task count. Single-Wave splits run everyone in parallel (the original behavior).` 를 다음으로 교체: +`**Phases 3–4 repeat per Wave in `## Waves` order.** A later Wave launches only after the previous Wave is fully approved; `` below = the *current* Wave's task count. Single-Wave splits run everyone in parallel (the original behavior).` 를 삭제하고, 그 자리에 다음을 넣는다: ```markdown **Phases 3–4 are one dispatch loop, not a per-Wave repeat.** Each round: @@ -609,33 +609,40 @@ Run: `npx bats tests/scripts.bats` → FAIL → **0** dispatch the printed ids, **2** nothing dispatchable but work is in flight (go wait for an event), **3** **DEADLOCK** — a failed dependency or a cycle: **do not wait**, report it and get a human decision (with no worker - running, no event can ever arrive), **4** the graph or status could not be - read — refuse, do not guess, **5** every task is terminal → go to Phase 5. -2. For each dispatched task run steps 1–3 below (O1–O5 on Orca). `` = the - number of tasks dispatched in THIS round. -3. Wait for an event. tmux: `scripts/watch-status.sh --tasks - impl_done 1` — without `--tasks` the tasks approved in earlier - rounds satisfy `expected=1` immediately and the wait spins. Orca: - `scripts/orca-wait.sh` unchanged; it is already event-driven. -4. On wake, handle that task (review → approved, or inject rework), then return - to step 1. - -**Write each brief at dispatch time.** It only needs the preceding signatures -that this task actually consumes, and by then those tasks are `approved`, so the -signatures are settled. + running, no event can ever arrive); after the human intervenes, return to + step 1 to re-run the check, **4** the graph or status could not be read — + refuse, do not guess; fix the error then re-run step 1, **5** every task is + in a terminal state → go to Phase 5. +2. For each dispatched task (`` = the number of tasks in this round): + - tmux: **0** (Preceding-interface injection) + steps **1–3** below (setup, + brief, launch, watch plan_ready). Orca: **O1–O5**. + - **1** `scripts/setup-worktrees.sh ...` then + verify with `git worktree list`. + - **2** Per task: write `briefs/.md` (templates/brief.md) — fill + `` and `` — then launch session and watch + until `plan_ready` (step 3 below). **Write the brief at dispatch time.** + It only needs the signatures this task consumes, and by then those are + `approved`, so they're settled. + - **3** Collect `plans/.md` when each session reaches `plan_ready`. +3. For each planned task, deliver §2 (implement) with `scripts/send-prompt.sh + send lo- ""` (tmux, see Phase 4 for exit-code branch logic), or + `orca orchestration task-create` the implement Task then + `scripts/orca-worker-start --task --terminal ` (Orca). + On delivery failure, re-run step 3 after fixing the error. +4. Wait for event. tmux: `scripts/watch-status.sh --tasks + impl_done ` — without `--tasks` the tasks approved in + earlier rounds satisfy `expected=` immediately and the wait spins. Orca: + `scripts/orca-wait.sh` with the implement Task ids, already event-driven. +5. On wake, handle that task: review each worktree diff (`git -C diff + ...HEAD`). If tests weak, audit with `test-quality-auditor`. On + approval, return to step 1 — whatever dependency it released shows up in the + next `ready-set.sh` round and the freed slot refills immediately. On rework + needed, write `reviews/-rN.md` and re-deliver with `send-prompt.sh + send` (or new Orca Task on same terminal); after 3 failed rounds, escalate. + When `ready-set.sh` returns **5**, go to Phase 5. ``` -- [ ] **Step 4: Phase 4의 Wave 문장을 고친다** - -`When this Wave's tasks are all approved, return to Phase 3 step 0 for the next Wave (inject its preceding-interface signatures); once the last Wave is approved, go to Phase 5.` 를 교체: - -```markdown -When a task is approved, return to step 1 of the dispatch loop — whatever -dependency it released shows up in the next `ready-set.sh` round and the freed -slot is refilled immediately. When `ready-set.sh` returns **5**, go to Phase 5. -``` - -- [ ] **Step 5: 재진입 절을 고친다** +- [ ] **Step 4: 재진입 절을 고친다** `## Re-entry (resume)` 의 첫 문장 뒤에 추가: @@ -645,9 +652,37 @@ There is no intermediate state such as a Wave index to restore. Reading the restored state — the same inputs always yield the same answer. ``` +- [ ] **Step 5: 강화된 테스트를 작성한다** + +`tests/scripts.bats` 의 contract test를 다음으로 교체해서 루프 구조를 검증한다: + +```bash +@test "SKILL.md: the dispatch loop structure pins step order and error handling" { + SKILL="${BATS_TEST_DIRNAME}/../skills/orchestrate/SKILL.md" + # Step 1 must run ready-set.sh first to decide what is dispatchable. + grep -qF '1. `scripts/ready-set.sh' "$SKILL" + # Step 1 exit codes must document what happens: exit 3/4 return to step 1, + # not "get a human decision" and vanish. + grep -qF 'after the human intervenes, return to step 1' "$SKILL" + grep -qF 'fix the error then re-run step 1' "$SKILL" + # Step 3 must deliver the implement prompt (was missing in v1); it is keyed + # off `send-prompt.sh send` on tmux or `task-create` on Orca. + grep -qF 'deliver §2 (implement)' "$SKILL" + grep -qF 'send-prompt.sh send' "$SKILL" + # Step 4 must wait, scoped to running ids via --tasks to avoid spin. + grep -qF 'watch-status.sh --tasks' "$SKILL" + # Step 5 must return to step 1 on approval, closing the loop. + grep -qF 'return to step 1' "$SKILL" + # Exit code 3 must be DEADLOCK and documented as "do not wait". + grep -qF 'DEADLOCK' "$SKILL" + # Waves must no longer be described as an execution barrier. + ! grep -qF 'A later Wave launches only after' "$SKILL" +} +``` + - [ ] **Step 6: 테스트 + 전체 스위트** -Run: `npx bats tests/` +Run: `npx bats tests/scripts.bats` + `npx bats tests/` Expected: 실패 0. - [ ] **Step 7: 커밋** diff --git a/skills/orchestrate/SKILL.md b/skills/orchestrate/SKILL.md index 6f64670..07a6259 100644 --- a/skills/orchestrate/SKILL.md +++ b/skills/orchestrate/SKILL.md @@ -139,20 +139,37 @@ answered. → **0** dispatch the printed ids, **2** nothing dispatchable but work is in flight (go wait for an event), **3** **DEADLOCK** — a failed dependency or a cycle: **do not wait**, report it and get a human decision (with no worker - running, no event can ever arrive), **4** the graph or status could not be - read — refuse, do not guess, **5** every task is in a terminal state → go to Phase 5. -2. For each dispatched task run steps 1–3 below (O1–O5 on Orca). `` = the - number of tasks dispatched in THIS round. -3. Wait for an event. tmux: `scripts/watch-status.sh --tasks - impl_done 1` — without `--tasks` the tasks approved in earlier - rounds satisfy `expected=1` immediately and the wait spins. Orca: - `scripts/orca-wait.sh` unchanged; it is already event-driven. -4. On wake, handle that task (review → approved, or inject rework), then return - to step 1. - -**Write each brief at dispatch time.** It only needs the preceding signatures -that this task actually consumes, and by then those tasks are `approved`, so the -signatures are settled. + running, no event can ever arrive); after the human intervenes, return to + step 1 to re-run the check, **4** the graph or status could not be read — + refuse, do not guess; fix the error then re-run step 1, **5** every task is + in a terminal state → go to Phase 5. +2. For each dispatched task (`` = the number of tasks in this round): + - tmux: **0** (Preceding-interface injection) + steps **1–3** below (setup, + brief, launch, watch plan_ready). Orca: **O1–O5**. + - **1** `scripts/setup-worktrees.sh ...` then + verify with `git worktree list`. + - **2** Per task: write `briefs/.md` (templates/brief.md) — fill + `` and `` — then launch session and watch + until `plan_ready` (step 3 below). **Write the brief at dispatch time.** + It only needs the signatures this task consumes, and by then those are + `approved`, so they're settled. + - **3** Collect `plans/.md` when each session reaches `plan_ready`. +3. For each planned task, deliver §2 (implement) with `scripts/send-prompt.sh + send lo- ""` (tmux, see Phase 4 for exit-code branch logic), or + `orca orchestration task-create` the implement Task then + `scripts/orca-worker-start --task --terminal ` (Orca). + On delivery failure, re-run step 3 after fixing the error. +4. Wait for event. tmux: `scripts/watch-status.sh --tasks + impl_done ` — without `--tasks` the tasks approved in + earlier rounds satisfy `expected=` immediately and the wait spins. Orca: + `scripts/orca-wait.sh` with the implement Task ids, already event-driven. +5. On wake, handle that task: review each worktree diff (`git -C diff + ...HEAD`). If tests weak, audit with `test-quality-auditor`. On + approval, return to step 1 — whatever dependency it released shows up in the + next `ready-set.sh` round and the freed slot refills immediately. On rework + needed, write `reviews/-rN.md` and re-deliver with `send-prompt.sh + send` (or new Orca Task on same terminal); after 3 failed rounds, escalate. + When `ready-set.sh` returns **5**, go to Phase 5. **Session knobs (tmux substrate, set once per run):** `export LO_RUN_ID=` so every `launch-session.sh` gets a collision-proof name `lo--` (reuse that diff --git a/tests/scripts.bats b/tests/scripts.bats index 61b59dd..1fe2a17 100644 --- a/tests/scripts.bats +++ b/tests/scripts.bats @@ -110,12 +110,23 @@ setup() { ! grep -qF 'concurrent-session cap** (default 4)' "$SKILL" } -@test "SKILL.md: the dispatch loop cites ready-set exit codes and --tasks" { +@test "SKILL.md: the dispatch loop structure pins step order and error handling" { SKILL="${BATS_TEST_DIRNAME}/../skills/orchestrate/SKILL.md" - grep -qF 'scripts/ready-set.sh' "$SKILL" - grep -q -- '--tasks' "$SKILL" - # exit 3 is the guard this design turns on; it must be spelled out as - # "do not wait", or it degrades into the silent stall it exists to prevent. + # Step 1 must run ready-set.sh first to decide what is dispatchable. + grep -qF '1. `scripts/ready-set.sh' "$SKILL" + # Step 1 exit codes 3 and 4 must explicitly return to step 1, not just report + # errors. Look for phrases showing re-entry after human intervention or fix. + grep -q 'return to' "$SKILL" && grep -q 'step 1' "$SKILL" + grep -q 're-run step' "$SKILL" + # Step 3 must deliver the implement prompt (was missing in v1); check that + # both tmux and Orca paths are mentioned. + grep -qF 'deliver §2 (implement)' "$SKILL" + grep -qF 'send-prompt.sh send' "$SKILL" + # Step 4 must wait, scoped to running ids via --tasks to avoid spin. + grep -qF 'watch-status.sh --tasks' "$SKILL" + # Step 5 must close the loop by returning to step 1 on approval. + grep -q 'approval' "$SKILL" && grep -q 'return to' "$SKILL" + # Exit code 3 must be DEADLOCK and documented as "do not wait". grep -qF 'DEADLOCK' "$SKILL" # Waves must no longer be described as an execution barrier. ! grep -qF 'A later Wave launches only after' "$SKILL"