vignette/docs/HANDOFF.md

788 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Vignette G0~G8 완료 추진 핸드오프
> Updated: 2026-08-09 KST (다른 에이전트 실행용 최종 인계 · G7 외부-proof P0 코드 완료 · current-source fixture 분할 GREEN/clean HEAD 재실행)
> Workspace: `D:\workspace\vignette`
> 이 문서는 다음 에이전트가 **G0~G8 아홉 목표 중 남은 G7 외부 게이트와 G8 NAS-origin 재검증을 닫기 위한** 실행 기준이다.
## 0. 현재 상태 한 줄 요약
**G0~G6의 내부 구현·증거는 종료됐다. 단, 최종 clean source 전체 회귀는 다시 실행해야 한다. G8 실제 rollback
receipt는 완료됐지만 current source clean-head 승격은 아직 0회다.
두 execute는 모두 NAS mutation 전에 안전하게 중단됐다. naive `started_at`의 9시간 오차 P0는 UTC/KST 계약과
실DB 브라우저 회귀로 수정됐고, 이제 새 clean commit 결속과 NAS-origin 전체 E2E가 남았다. G7 공개 런처의
local sidecar lifecycle·provider/model readiness·Uvicorn queue, fresh PID/source provenance, 3,120초 runner와
3,000초 공통 시간창·canonical exit 계약은 소스에서 완료됐지만, current source 공개 배포와 외부 4-artifact 게이트는 남아 있다.**
| Goal | 판정 | 완료 증거 |
|---|---|---|
| G0 Measurement Foundation | DONE | provenance census 29/29, unknown/missing/orphan/null/total violations 0 |
| G1 Alliance Core | DONE | prompt 1.2 read-skew fix, prediction 24/24 ready, direction 9/9, error 0, recall 94.4%, precision 73.9% |
| G2 Outcome Trajectory | DONE | benchmark accuracy/early-warning recall 1.0, false alert 0, 실 DB 10 sessions/30 measurements |
| G3 Rupture & Repair | DONE | macro-F1 1.0, status 1.0, gaming 0, missed→partial→resolved 실 DB/API |
| G4 Deliberate Practice | DONE | completed-session observer, actual API/DB attempts, teacher correction, returned-practice browser 폐루프 |
| G5 Calibration & Transfer | DONE | actual transfer execution, independent/model-run/instrument/evidence provenance, DB/browser idempotency |
| G6 Supervision & Research | DONE | safety event metadata-only projection, safety priority 1 > deterioration 2, raw detail 0 |
| G7 Multimodal Alliance | **internal DONE · external GATE** | consent/withdrawal, synthetic soak 59/59, G7 runner/checker/topology 86/86, launcher/sidecar 80/80, API voice 71/71 |
| G8 Continuous Improvement | **receipt DONE · runtime REVALIDATION** | 실제 image rollback 2회 + executed receipt 2건. timestamp 수정 실DB GREEN, 새 clean commit·NAS-origin E2E 미완료 |
대시보드 status count는 **32 done / 3 doing / 0 planned**다.
Git 기준선은 branch `master`, HEAD `94c681d450d2f3b4df55c036e333cd817db3f7fb`이고 `origin/master`보다 2 commit
앞서 있다. 공유 worktree에는
문서/대시보드/G7·G8 런북, 완결된 clean-head 릴리스 에이전트·테스트, UTC/KST session timestamp 수정과
browser 회귀가 tracked dirty로 남아 있다. Git 작업은 stage/commit/push하지 않았다.
### 이 문서가 인계된 정확한 중단점
- 사용자가 **현재 구현을 잠시 멈추고 다른 에이전트로 넘길 핸드오프를 만들라**고 요청했다. 핸드오프 정리 중
public/NAS/DB/마이크/프로세스의 수동 mutation은 0회다. 5분 Scheduled Task는 한 번씩 자동 실행됐으나 아래
source-pin gate에서 안전하게 exit 1로 중단됐다.
- active goal은 계속 `active`다. 목표는 “문서 작성”이 아니라 **G0~G8 아홉 목표를 실제 runtime과 E2E로
모두 닫는 것**이다. G7 canonical checker exit 0과 G8 current clean source의 NAS-origin 0-failure가 없으면
`goal complete`를 호출하지 않는다.
- 마지막 완료 작업은 G7 runner/checker/topology·fresh public launcher provenance P0, session timestamp UTC/KST 계약,
API full 921/921, gateway 58/58, Web API types·typecheck·build, 자기주도 route fixture desktop/mobile 6/6이다.
fixture desktop/mobile은 마지막 단일
120/120 뒤 dashboard spec/HTML이 다시 바뀌었고 그 변경의 dashboard 10/10을 별도로 확인했으므로,
**현재 트리는 불변 110 + 최신 dashboard 10의 분할 GREEN**이다. 새 clean HEAD에서 exact 120을 다시 실행한다.
- G7 Windows topology mode, 3,000초 공통 시간창, canonical checker-bound exit와 공개 런처의 local
Whisper/MeloTTS lifecycle·exact readiness·Uvicorn queue·fresh API/cloudflared provenance 계약은 코드와 회귀가
끝났지만 public launcher 실행과 capture는 수행하지 않았다.
현재 public API가 Git metadata가 없는 냉동 release 디렉터리에서 실행되므로 provenance gate가 의도적으로
닫혀 있다. 이 검증을 약화해서 통과시키면 안 된다.
- **public task source-pin P0:** watchdog·로그온 boot는 이제 detached clean release만 허용하고 Git
commit/tree와 watchdog|boot/start script SHA를 health·Docker·process mutation 전에 검증한다. hidden VBS도
shared source를 직접 실행하지 않고 구조적으로 pin된 task만 trigger한다. 회귀는 provenance 11개를 포함한
watchdog/start/cutover combined 22/22, PowerShell 5.1 parser·WSH syntax PASS다. 그러나 **실제 두 Scheduled Task action은
아직 shared worktree를 가리키며 pin 인자가 없다.** 5분 watchdog은 pin 부재로 반복 실행마다
`LastTaskResult=1`이고, 새 필수 pin 인자 부재 때문에 운영 프로세스를 건드리기 전에 의도대로 fail-closed했다.
18:02:55 KST에는 Docker Desktop UI가 active DB container에 직접 stop을 보내 public `db=false`가 재발했다.
같은 container ID와 recovered named volume만 18:05 KST 재시작했고, 공개 local/public health는 다시
`status=ok, db=true, engine=true`다. clean commit 뒤 두 task를 동일한 detached stable
release root로 재등록하고 action pin·watchdog result 0·로그온/reboot smoke를 확인하기 전에는 public runtime
자동복구를 DONE으로 부르지 않는다.
- 다음 에이전트의 첫 mutation은 **사용자 승인 아래 새 clean commit 생성**이어야 한다. 그 clean source만 G8 NAS
preview 또는 G7 public runtime의 배포 입력으로 쓰고, 두 환경을 동시에 변경하지 말며 mutation owner는 한 명만 둔다.
### 완료 조건 판정 (2026-08-09 재검증)
원래 active goal의 완료 선언 조건 네 가지를 실행으로 대조한 결과다.
| # | 조건 | 판정 | 근거 |
|---|---|---|---|
| 1 | G0~G8 각각의 코드 계약과 실제 runtime 증거 | **부분 충족** | G0~G6 충족. G8 receipt는 충족했지만 NAS-origin 회기 재검증이 남았고 G7 외부 GATE가 열려 있다 |
| 2 | 학생 홈→회기→리뷰→처방→재연습→전이 폐루프가 실제 API/DB/browser 통과 | **충족** | 일회용 클론에서 `returned-practice-db-closed-loop` desktop 2/2 + mobile 2/2, route mock 0 |
| 3 | current source 증거와 대시보드·TODO·backlog 일치 | **충족** | SSOT checker FAIL 0, unit 5/5, dashboard E2E 10/10, count 32/3/0이 카드와 일치 |
| 4 | G7을 가짜·합성 증거로 DONE 처리하지 않음 | **충족** | `check-g7-external-proof.py` exit 1 유지, G7 카드는 `GATE` |
재검증 수치: API **921 passed**, gateway **58**, executor **28**, probe **11**, helper launcher **37**,
release agent **30**, G7 runner/checker/topology 묶음 **86**, G7 public launcher/sidecar **80**, G7 통합 **166**,
API voice 통합 **71**,
MeloTTS 사이드카 **16**, local whisper 사이드카 **38**,
SSOT FAIL **0**,
dashboard E2E **10/10**, web api-types·typecheck·build 통과, ruff clean.
current-source fixture 묶음은 16:49 KST 단일 실행에서 **120/120 PASS(92.0s)**했다. dev dashboard file URL은 실행 cwd가
아니라 spec의 `import.meta.url`로 repo root를 찾으며, session-review fixture는 alliance pulse까지 소유한다. 관리자
live health 테스트는 React StrictMode 중복 조회의 첫 응답을 고정하지 않고 화면이 실제 반영한 성공
`/admin/users` 스냅샷과 KPI를 결속한다. 그 뒤 dev-dashboard spec/HTML이 변경됐고 최신 dashboard 10/10만 따로
재검증됐다. 따라서 current tree의 정직한 판정은 **110 불변 + 10 최신 분할 GREEN**이며, 새 clean commit에서
같은 120건을 한 번 재실행해 archive/source 결속 뒤의 결과를 고정한다.
**따라서 `update_goal complete`를 호출하지 않는다.** 조건 1이 G7과 G8 NAS-origin 재검증 때문에 미충족이고,
조건 4가 G7을 합성 증거로 메우는 것을 금지한다. G8 receipt 자체는 종료됐다(§3).
### G0~G8 목표별 인수 기준
| Goal | 원 계획의 종료 조건 | 현재 인정 가능한 증거 | 다음 에이전트가 최종 종료 전에 할 일 |
|---|---|---|---|
| G0 | 100% provenance, evaluator/client 누수 0, Python/TS/DB schema conformance, 기존 회기 무회귀 | current producer census 29/29, 위반 0, migrations 14~16 2회 멱등 | current HEAD 전체 backend/schema 회귀와 최종 census를 다시 실행 |
| G1 | goal/task/bond 독립 저장, 축별 UI, gold 근거 span, 평가 실패 fail-closed | prediction 24/24 ready, 방향 9/9, error 0, recall 94.4%, precision 73.9%, pre/mid/post UI | current HEAD의 alliance desktop/mobile·DB-backed session 경로 재실행 |
| G2 | 5회기 연속성, 조기 경보 recall, false alert/uncertainty, synthetic 비임상 경계 | accuracy/recall 1.0, false alert 0, 실제 DB 10 sessions/30 measurements | current HEAD outcome trajectory API/UI 회귀 재실행 |
| G3 | rupture 유형 benchmark, missed/partial/resolved E2E, gaming/암기 방지 | macro-F1/status 1.0, gaming 0, 실제 DB/API missed→partial→resolved | text/SSE 회기 및 rupture UI desktop/mobile 재실행 |
| G4 | 모든 코칭 카드의 실행 재연습, 전후 근거 저장, unseen 전이 전 mastery 금지 | 실제 API/DB attempt·observer·teacher correction, browser route mock 0 | disposable DB에서 returned-practice desktop/mobile 4/4와 멱등 재확인 |
| G5 | 자기평가 잠금, calibration 감소, unseen transfer 유지, subgroup drift | actual transfer execution과 independent/model-run/instrument/evidence provenance | G4와 같은 disposable DB에서 actual POST·read reload·1→1 멱등 재확인 |
| G6 | 3-click queue, 원장 drilldown, 버전 재현, 역할/RLS/audit | safety metadata-only, safety priority 1 > deterioration 2, raw detail 0 | current HEAD producer/store/API와 supervisor UI 회귀 재실행 |
| G7 | 시간 정렬, **물리 마이크+공개 WSS 장시간 E2E**, text-only 대비 음성 이득, 동의/삭제 정책 | 내부 계약·synthetic soak만 완료. canonical checker는 의도적으로 exit 1 | current 공개 배포→인증 WSS→명시 동의 mic 50분→동시 runtime/topology→독립 human pack→checker exit 0 |
| G8 | source→draft→red-team→benchmark→catalog 재현, 누수/PII/무근거 0, 자동 calibration+rollback, incident→test/backlog 환류 | agentic worker·human gate·실제 rollback/restore receipt 2건, timestamp focused 실DB/browser GREEN | 사용자 승인 아래 새 clean commit→NAS 배포→**실제 NAS origin** 전체 회기 E2E GREEN→SSOT 동기화 |
## 1. 작업 시작 전 반드시 읽을 파일
1. `AGENTS.md`
2. `docs/README.md`
3. `docs/HANDOFF.md` — 현재 문서
4. `docs/dev_dashboard.html` — 상태 SSOT
5. `docs/TODO.md` — 열린 작업 SSOT
6. `docs/ops/backlog-2026-06-26.md`
7. `docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`**다음 작업의 핵심**
8. `docs/ops/nas-preview-g8-rollback-proof-runbook.md`
9. `docs/ops/nas-preview-deployment-evidence-2026-08-07.md`
10. `docs/ops/public-db-recovery-rehearsal-2026-08-07.md`
`docs/archive/`는 현재 상태 근거로 읽지 않는다.
## 2. 절대 보존 경계
- Windows 11 + PowerShell 환경이다. 실행 전 OS·셸·도구 경로를 다시 확인한다.
- 명령은 PowerShell 5.1 호환 문법을 기본으로 작성한다. NAS 원격 스크립트는 **UTF-8 base64 전달 방식**을 쓴다.
(`$()`가 PowerShell에서 먼저 확장되는 것을 막는다.)
- 이 핸드오프 작성 시 branch/HEAD는 `master` / `94c681d450d2f3b4df55c036e333cd817db3f7fb`이고 include 후보는
52개다: UTC/KST timestamp, G7 voice/topology/public launcher, pinned `psutil`, G8 clean-head controller, public task source pin,
학생 boot recovery, current fixture 안정화, dashboard/TODO/backlog/HANDOFF·런북 동기화다. 이 묶음들은 아직
commit에 들어 있지 않다.
- clean-head 구현은 unit 30/30, G7 runner/checker/topology는 86/86, G7 public launcher/sidecar는 80/80,
public task source pin은 provenance 11개를 포함한 combined 22/22, timestamp·voice 계약을 포함한 API는
921/921와 KST 실DB browser 1/1, current fixture는 110 불변 + 최신 dashboard 10 분할 GREEN,
SSOT는 FAIL 0와 dashboard E2E 10/10을 통과했다. 다음 에이전트는 최초 diff review 뒤 동일한 focused gate를
다시 실행하고, 서로 다른 lane의 파일을
누락한 채 clean commit을 만들지 않는다.
- 기존·watchdog 생성 엔진 로그 4개와 `apps/web/test-results/`의 PNG 2개는 commit에서 제외하고 보존한다. 신규
`apps/api/app/test_session_read_model.py`
`docs/ops/evidence/g8-clean-head-material-milestone-2026-08-09.json`은 이번 목표 산출물이므로 clean source에
포함해야 한다.
시작 즉시 `git status --short`로 다시 확인하고 `git reset --hard`, `git checkout --`, 대규모 자동 포맷, 임의 삭제는 금지한다.
- Git commit/stage/push는 사용자가 요청하기 전에는 하지 않는다.
- 공개 복구 DB, 원본 volume, pre-recovery container, dump, NAS named volume, 이전 API/Web 이미지를 삭제하지 않는다.
- **로컬 포트 8001(공개 API origin) · 55432(공개 DB) · 9099/9100(엔진)은 공개 런타임이다.**
공개 Web은 Cloudflare Pages이고 5174는 `vnet.18ka.net`용 로컬 preview다. `scripts/dev-up.ps1`은 런타임
리스너를 정리할 수 있으므로 공개 런타임이 떠 있는 동안 실행하지 않는다.
- 비밀값, 이메일, cookie, 사용자 UUID가 포함된 raw response, `.env` 내용은 로그·증거·채팅에 출력하지 않는다.
## 3. G8 receipt — 종료됨 · NAS-origin runtime 재검증은 열림
### Gate6 계약 정정 (성립 불가능했던 이전 계약을 코드로 교체)
감사 대상 current API 이미지 `sha256:52e0e816…8b2d``com.docker.compose.project=vignette-preview-20260807`,
`service=api`, `version=2.20.1` **image label**을 갖고 있다. 따라서 "helper의 `com.docker.compose.*` key 0개"는
감사되지 않은 다른 이미지를 쓰지 않는 한 성립하지 않는다. 계약을 **key 부재 → 소속(membership)** 으로 바꿔
구현했다.
- 코드: `scripts/launch-nas-preview-g8-helpers.py`
- 테스트: `scripts/test_launch_nas_preview_g8_helpers.py`**37/37**
- 계약: image 상속 label을 baseline으로 읽고 container의 모든 compose label이 baseline과 같거나 선언된 격리
override인지 검사 · 최종 project ≠ target · 최종 service ∉ {api,web,db,proxy} · argv에 target project/service
label 미주입(fake-runner) · exact container name/ID/`docker inspect` 증거 보존
- 런북: `docs/ops/nas-preview-g8-rollback-proof-runbook.md`
### 실제 실행 결과
| plan | receipt | 활성화된 API/Web 이미지 |
|---|---|---|
| `rollback-old` | `nas-g8-723eeef22eab05e63e3fafb0` | `79ec…4450` / `c530…2f28` |
| `restore-current` | `nas-g8-2738846cf2cf4fbe8ce0fc26` | `52e0…8b2d` / `6fdb…f215` |
- release gate·approval 각각 2회 멱등, lifecycle `executed`, artifact/approval/receipt binding, control-plane 분리 검증
- DB: `audit.ci_lifecycle_event` `rollback/executed` 2 · `audit.ci_human_approval_event` `authorize_rollback` 2 ·
`app.ci_release_gate` 2(전부 `pending_human_approval`, silent auto-promotion 0) · `ci_gate_artifact` 8
- HMAC journal 6 records, `previous_hash` 체인 전수 PASS, SHA256 `a5594feb…0690`
- 최종 상태: health 3/3, OpenAPI 126, auth 401, Web 200, helper 0, listener 0, 비밀 env 파기
- **계획 이탈:** Windows SSH 터널 `18018→8018`은 NAS sshd가 `administratively prohibited`로 direct-tcpip를 거부해
쓸 수 없었다. **sshd 설정은 바꾸지 않았고**, 같은 격리 계약의 NAS-side probe 컨테이너에서 loopback으로
실행해 control-plane(8018)/preview(8088) origin 분리는 그대로 유지했다. 다음 에이전트도 NAS에서
포트 포워딩을 기대하지 말 것.
- 기계 판독 증거: `docs/ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json`
## 4. 공개 서비스 복구 상태 — 이미 완료, 다시 망가뜨리지 말 것
공개 계정·회기 DB 복구는 2026-08-07 18:26 KST에 완료됐다. 2026-08-09 14:03 KST에는 Docker Desktop이
중단돼 DB listener가 사라지면서 public health가 일시적으로 `db=false`가 됐고, 18:02:55 KST에는 Docker Desktop
UI가 active DB container에 직접 stop을 보내 같은 증상이 재발했다. 두 번 모두 보존된 동일 컨테이너·볼륨만
재기동해 복구했다. 새 컨테이너·볼륨 생성이나 데이터 복원 덮어쓰기는 없었다.
- Public Web `https://vignette.chanpaca.net` · Public API `https://api-vignette.chanpaca.net` (origin `127.0.0.1:8001`)
- active DB container `vignette-dev-db`, volume `vignette_recovered_prod_20260807`, restart `unless-stopped`
- cutover 기준 복구 집계: users 84, sessions 30, turns 705, Google users 16, orphan sessions 0
- 2026-08-09 18:05 KST 재기동 후 owner read-only 집계: users 356, sessions 193, turns 726, Google users 16,
Google-owned sessions 31. 조회는 `BEGIN READ ONLY``ROLLBACK`했고 PII를 출력하지 않았다.
- health `environment=prod`, `db=true`, `engine=true`, `engine_mode=claude_cli`, dev-login disabled, unauth `/auth/me` 401
- **2026-08-09 18:05 KST recovery snapshot:** local/public health는 `status=ok, db=true, engine=true`, Google OAuth
start는 302→`accounts.google.com`, 공개 OpenAPI는 119 paths다. `/voice/health`는 아직 OpenAI STT/TTS이고
local Whisper 9882·MeloTTS 9883 listener는 0이다.
- DB 컨테이너는 `vignette_recovered_prod_20260807` named volume과 `unless-stopped`를 유지하고 local/public
health는 `status=ok, db=true, engine=true`다. 다만 watchdog·로그온 task action은 아직 shared worktree이며
stable root/commit/tree pin 인자가 없다. 새 source-pin 코드가 5분 watchdog을 mutation 전에 차단해 반복 실행이
`LastTaskResult=1`이다. clean commit의 detached release root로 두 task를 재등록하기 전까지 자동복구가
의도적으로 비활성인 상태다.
보안상 과거 `auth_session`은 복원하지 않았다. 사용자는 Google 재로그인이 필요하다.
보존물: pre-recovery container `vignette-dev-db-pre-recovery-20260807-182642`과 원본/recovery Docker volumes.
과거 증거에는 pre-cutover dump `…20260807-091057Z.dump` SHA256 `6b84d5c8…af1f`, recovered dump
`…20260807-092630Z.dump` SHA256 `d7bcc396…68ed`, post-cutover dump `…20260807-095511Z.dump` SHA256
`660695c8…65b88`이 기록돼 있으나, **인계 시점 로컬에는 `D:\workspace\vignette-backups` 디렉터리가 없고
`D:\workspace\vignette-recovery`에도 이 dump들이 보이지 않는다.** 삭제로 단정하지 말고 off-host/NAS/다른 경로를
확인하기 전 restore·cleanup을 금지한다. NAS의 pre-proof/pre-clean-head dump는 §5 기준으로 실제 존재와 hash를
재확인했다.
공개 DB에는 current dirty source를 배포하지 않는다.
## 5. NAS 배포 기준선 — 현재 HEAD가 아니라 2026-08-07 릴리스
- **2026-08-09 17:07 KST read-only snapshot:** URL `http://100.116.83.60:8088` · project
`vignette-preview-20260807` · remote root `/volume1/docker/vignette-preview-20260807` · active release SHA
`6030a677af7e87cbfabc422b553d108d53414fd3c446548734a13b036d35c611`.
- API `sha256:52e0e816…8b2d` / Web `sha256:6fdbb646…f215` exact image가 실행 중이고 previous
`79ec731f…0450` / `c530213f…62f28`도 보존돼 있다. Compose `api/web/db/proxy` 4개는 모두 running,
restart policy는 `unless-stopped`다.
- DB volume `vignette-preview-20260807_pgdata` · pre-proof dump `vignette-preview-g8-preproof-20260807T110610Z.dump`
SHA256 `92ed4736…1570f`, 960,970 bytes · pre-clean-head dump
`vignette-preview-pre-clean-head-20260809T051611Z.dump` SHA256 `726982…5d41c`, 1,015,222 bytes.
- health 3/3(`db=true`, `engine=true`), auth 401, OpenAPI 126 + G1~G8 route, root/JS/CSS 200이다.
- helper container 0, NAS listener 8018/18149 0, Windows tunnel listener 18018 0이다.
- **P1 권한 관측:** remote release root와 Compose 파일이 mode `0777`이다. 다음 execute 전에 owner/mode를
최소권한으로 좁히고 release agent preflight가 이를 fail-closed로 검증해야 한다. 현재 상태를 안전한 배포 신뢰
경계로 간주하지 않는다.
- localhost candidate browser E2E 108/108(634.578s), postdeploy SSE→DB review PASS
- **G8 rollback/restore 이후 재검증:** 실제 브라우저 SSE→DB review 재실행 PASS(35.0s)
배포 중 첫 108 gate가 103/5로 fail-closed된 이력(G5 fixture drift, StrictMode retry fixture, `shell.css` manifest 누락)은
지우지 않는다.
### ⚠ 배포된 SHA는 비-secure origin 결함을 갖고 있다 (수정은 소스에만 있음)
배포 후 NAS 프리뷰(`http://100.116.83.60:8088`, 평문 HTTP·비-localhost)에 전체 회기 스펙을 돌려보니 24건이
실패했고, 원인은 단 하나였다. 페이지 스냅샷의 실제 예외는 `crypto.randomUUID is not a function`이다.
이 API는 **secure context(HTTPS 또는 localhost)에서만** 노출되는데 제품 코드 18곳이 fallback 없이 직접
호출했고, `RuptureRepairCard.tsx`는 렌더 시점(`useRef`)에 호출해 회기 리뷰 라우트 전체가 error boundary로
떨어졌다. 릴리스 게이트의 108/108은 **localhost 후보 스택**(secure context)에서 돌았기 때문에 이 경로를
한 번도 밟지 않았다.
- 수정: `apps/web/src/lib/uuid.ts``randomUuid()`로 통일. fallback도 `crypto.getRandomValues`를 우선
사용해 idempotency key의 예측 불가능성을 유지하고, Web Crypto가 아예 없을 때만 `Math.random`으로 내려간다.
- 회귀: `apps/web/e2e/insecure-context-uuid.spec.ts` **6/6** (직접 호출 0건 검사 포함), typecheck·build 통과.
- **아직 NAS에 배포하지 않았다.** 배포된 SHA `6030a677…c611`은 여전히 결함 빌드다. 다음 배포 때
release agent로 승격하고, 승격 뒤에는 NAS origin에서 회기 스펙을 다시 돌려 24건이 사라지는지 확인한다.
- 교훈: candidate gate를 localhost에서만 돌리면 secure-context 전용 API 결함을 못 잡는다. 배포 대상과
같은 scheme/host 형태에서 최소 한 번은 회기 리뷰 라우트를 열어봐야 한다.
## 6. 다음 실행 순서 — G8 runtime 재검증 후 G7 외부 종료
### 6.1 G8: clean-head 릴리스 경로부터 완성
`scripts/run-outcome-os-release-agent.py`의 명시적 clean-head 경로는 구현됐다. 기존 patch mode를 기본값으로
보존하며, tracked-clean source worktree의 exact HEAD/tree/archive만 candidate로 사용한다. 수동 복사나 NAS 직접
빌드는 계속 금지한다.
구현된 **`--source-mode clean-head`** 계약은 다음과 같다.
1. tracked dirty가 있으면 fail-closed. untracked 로그·테스트 결과는 archive에서 제외한다.
2. exact Git HEAD, tree, `git archive` SHA를 증거에 기록한다.
3. 같은 HEAD의 archive를 두 번 만들어 SHA가 동일한지 검증한다.
4. candidate는 그 archive만 풀어 만들고 patch/manifest를 암묵 적용하지 않는다.
5. 기존 preflight·API full·Web type/build·release E2E·DB snapshot·rollback 절차는 그대로 유지한다.
6. Vite source 전용 `e2e/insecure-context-uuid.spec.ts`는 별도 localhost gate(5198)로 6/6 실행하고,
production candidate/NAS-origin은 실제 배포 번들에서 동작하는 11개 회기 스펙만 실행한다.
7. unit/fake runner가 tracked dirty, archive SHA drift, evidence binding, patch-mode 무회귀를 검증해야 한다.
검증: release-agent unit 30/30, source-only UUID gate 6/6, Ruff, py_compile, `git diff --check` PASS. 공유 worktree에는
인계 문서와 controller 변경이 있으므로 이 경로 자체가 의도대로 tracked-dirty를 차단한다. 실제 배포는 새 commit을
가리키는 별도 detached clean worktree를 만들고 `--source-repo-root <clean-worktree>`로 지정한 뒤
dry-run→execute 순서로 수행한다.
#### clean commit preflight — include 52 / exclude 6
2026-08-09 최종 경로 목록 SHA-256은 ordinal 정렬된 UTF-8 LF 경로를 줄바꿈으로 연결하고 마지막 개행 없이 계산해
include `4d94694ccf9def6945272c61f673cac0eaa9fb59f4f9b6c165d661d819f891a1`, exclude
`1ff8c26732772df6b5857bf84de01a53d7da9c3b7d52df08938e77f3a527f022`다. 아래 include 52개만
사용자 승인 후 **명시적으로** stage한다. 그 직전 `git status --porcelain=v1 -uall`로 목록·해시를 다시 계산하고,
새 경로가 하나라도 있으면 멈춘다. `git add -A`는 금지한다.
```text
apps/api/app/config.py
apps/api/app/routes/voice.py
apps/api/app/services/session_metrics.py
apps/api/app/services/voice.py
apps/api/app/session_read_model.py
apps/api/app/test_session_read_model.py
apps/api/app/test_voice_service.py
apps/api/app/test_voice_ws.py
apps/api/requirements.txt
apps/web/e2e/admin.spec.ts
apps/web/e2e/dev-dashboard.spec.ts
apps/web/e2e/self-directed-learning-loop.spec.ts
apps/web/e2e/session-mvp.spec.ts
apps/web/e2e/session-persistence.spec.ts
apps/web/e2e/session-review-fixture.ts
apps/web/e2e/session-review.spec.ts
apps/web/index.html
docs/HANDOFF.md
docs/TODO.md
docs/decisions/local-voice-stack.md
docs/dev_dashboard.html
docs/guides/local-development.md
docs/guides/testing.md
docs/ops/backlog-2026-06-26.md
docs/ops/evidence/g8-clean-head-material-milestone-2026-08-09.json
docs/ops/nas-preview-g8-rollback-proof-runbook.md
docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md
docs/ops/public-runtime-watchdog.md
scripts/boot-public-runtime.ps1
scripts/capture-g7-topology-evidence.py
scripts/check-dev-dashboard-ssot.py
scripts/check-g7-external-proof.py
scripts/install-public-runtime-task.ps1
scripts/local-whisper-stt-server.py
scripts/melotts-server.py
scripts/probe-public-voice-sidecars.py
scripts/register-boot-task.ps1
scripts/run-g7-external-proof-window.py
scripts/run-outcome-os-release-agent.py
scripts/start-local-whisper-stt.ps1
scripts/start-public-runtime.ps1
scripts/test_g7_external_proof.py
scripts/test_g7_topology_evidence.py
scripts/test_local_whisper_stt_server.py
scripts/test_melotts_server.py
scripts/test_outcome_os_release_agent.py
scripts/test_public_runtime_watchdog_provenance.py
scripts/test_public_voice_sidecar_probe.py
scripts/test_run_g7_external_proof_window.py
scripts/test_start_public_runtime_contract.py
scripts/watch-public-runtime-hidden.vbs
scripts/watch-public-runtime.ps1
```
아래 6개와 ignored `infra/.env.nas-preview`는 절대 stage하지 않는다. NAS 실행 시 env는
`--nas-env-file D:\workspace\vignette\infra\.env.nas-preview`로 외부 주입한다.
```text
apps/api/engine.public.err.log.2026-08-07T2200.bak
apps/api/engine.public.err.log.20260809-160042.bak
apps/api/engine.public.out.log.2026-08-07T2200.bak
apps/api/engine.public.out.log.20260809-160042.bak
apps/web/test-results/g7-voice-consent-desktop.png
apps/web/test-results/g7-voice-consent-mobile.png
```
#### 2026-08-09 clean-head 실행 결과 — NAS mutation 0
- dry-run `D:\workspace\vignette-recovery\g8-clean-head-dry-run-20260809.json`: `ok=true`,
active `6030a677…c611`, desired archive `6b1b15bd…a3f7`, `deployment_mutated=false`.
- execute 1 `...\g8-clean-head-execute-20260809.json`: Windows Git archive의 CRLF 때문에 생성 API 타입 byte check가
실패했다. archive SHA 검증 후 candidate의 생성 타입/셸 스크립트만 LF로 정규화하도록 controller를 수정했다.
NAS mutation 0, rollback 불필요.
- execute 2 `...\g8-clean-head-execute-20260809-r2.json`: API/types/typecheck/build/candidate stack은 통과했고
Playwright는 **109 passed / 5 failed**였다. source-only UUID 스펙 4건은 production build에서
`/src/lib/uuid.ts`를 import할 수 없는 gate 배치 오류라 별도 Vite gate로 분리했다. 남은 1건은 아래 제품 버그다.
NAS mutation 0, rollback 불필요. 동일 전체 execute를 아직 다시 시도하지 않았다.
#### 2026-08-09 clean-head `5221f79e` 실행 결과 — NAS mutation 0
- exact include 52개를 Yun Chan으로 커밋했다. HEAD `5221f79e…1c69`, tree `e4f15001…308b`, 2회 동일 archive
`1109bf86…0f4b`(482,662,400 bytes)를 detached clean worktree에서 고정했다.
- 새 milestone과 dry-run은 active `6030a677…c611` → desired `1109bf86…0f4b`,
`deployment_mutated=false`로 통과했다. 실행 직전 NAS custom dump
`6f4b95a7…b529f`(1,015,222 bytes, TOC 1,752 / TABLE DATA 129, 전용 pgdata volume)를 생성했다.
- execute는 API 921, API types, typecheck, build, insecure-context 6/6, candidate stack과 DB-backed
single-run 후반부까지 통과했지만 **110 passed / 2 failed**에서 승격 전에 중단됐다. 실패는 제품 화면이 아니라
`React 부트가 실제로 비어 있으면 진단을 유지한다`의 desktop/mobile 두 복제였다.
- 원인은 테스트가 Vite 개발 entry `/src/main.tsx`만 차단해 production candidate의 hashed entry
`/assets/index-*.js`를 차단하지 못한 환경 계약 누락이다. 두 entry를 함께 차단하도록 수정했고 Vite source 2/2와
production preview 2/2를 각각 통과했다. 전체 execute를 즉시 반복하지 않고 이 수정의 새 clean commit·archive를
다시 결속한 뒤 한 번만 재실행한다.
#### P0 수정 완료 — 회기 timestamp UTC 전송과 KST 표시 날짜
- 최초 실패 스펙: `session-persistence.spec.ts``persists AI tutor coaching history through reload @single-run`.
- 코칭 생성·DB 저장·reload 후 `C` 마커까지는 성공했지만, 수정 전 화면이 `9:03:59 / 543:59` 경과로 계산해
`회기 시간이 종료됐어요` 모달을 띄웠고 그 모달이 코칭 마커 클릭을 가로막았다.
- 원인: `apps/api/app/session_read_model.py::iso()`
`datetime.fromtimestamp(ts).isoformat(timespec="seconds")`로 timezone 없는 문자열을 반환한다.
`apps/web/src/pages/Session.tsx::elapsedFromSession()``Date.parse(detail.started_at)`로 이를 브라우저 로컬 KST로
해석한다. Linux API UTC와 KST 브라우저 사이에서 약 9시간 오차가 생긴다.
- 수정: `session_read_model.iso()`와 dashboard용 `session_metrics.iso_datetime()`은 UTC `+00:00`을 반환한다.
회기 리뷰 달력 날짜는 서버 timezone이 아니라 고정 KST(+09:00)를 사용한다.
- backend 관련 65 passed, 후속 voice 계약을 포함한 API 전체 **921 passed**, Ruff·compile·web typecheck가 통과했다.
- 고유 Compose project/volume의 실제 DB/API/engine/browser에서 KST context로 focused 1/1 PASS했다. UTC suffix,
`status=active`, `ended_at=null`, 회기 나이 10분 미만, 화면 `00:00-09:59`, timebar/dialog 0,
coach mark 실제 클릭과 DB history/source-pack dialog를 모두 검증했다. 종료 뒤 container/volume/listener는 0이다.
- **테스트 강제 클릭이나 모달 닫기 우회는 사용하지 않았다.** 첫 clean source `5221f79e…1c69`에는 제품 수정과
controller 변경이 포함됐고, production entry 차단 회귀 2건의 테스트 수정만 후속 clean commit으로 다시 고정한다.
### 6.2 G8: NAS preview 배포와 실제 origin E2E
clean-head release mode가 GREEN이면 전용 NAS preview만 대상으로 백업→dry-run→배포→검증한다. 공개 DB·공개 API는
이 단계에서 건드리지 않는다. 성공 조건은 다음 전부다.
직전 새 backup은
`/volume1/docker/vignette-preview-20260807/backups/vignette-preview-pre-clean-head-20260809T051611Z.dump`,
SHA-256 `72698232d132a8f848d81ff9d8602430e7e623c7646cc236cb39f978aab5d41c`,
1,015,222 bytes, TOC 1,752다. 하지만 release agent 자체에는 `pg_dump`/`pg_restore` 실행 계약이 없다.
따라서 **매 execute 직전** 별도 승인된 백업 단계에서 fresh custom dump를 만들고 SHA-256·byte size·TOC parse/count와
current DB identity를 증거에 결속한 뒤에만 승격한다. 기존 dump 존재만으로 이 단계를 생략하지 않는다.
release agent의 자동 rollback 범위는 exact API/Web image, Compose 적용 상태, active-state 파일뿐이다. 이미 적용된
DB migration이나 데이터 변경은 자동 복구하지 않는다. migration/data restore는 별도 owner 승인·대상 DB identity·dump
hash를 갖춘 복원 절차로만 수행한다. 이미지 rollback 성공을 DB rollback 성공으로 기록하면 안 된다.
- 새로운 active SHA와 exact API/Web image ID, predeploy dump SHA, previous image 보존.
- health 3/3, db/engine true, auth 401, OpenAPI와 G0~G8 route 존재.
- `http://100.116.83.60:8088` **실제 평문 NAS origin**에서 §8의 회기 E2E 묶음 0 failure.
- `crypto.randomUUID` 예외 0, error boundary 0, SSE→DB review PASS.
- 학생 홈→회기→리뷰→G4/G5 반환 연습의 desktop/mobile·DB read reload·멱등·UUID 비노출 유지.
- 실패 시 자동 rollback과 previous image 복구를 검증하고, 성공 전에는 G8 카드 전체를 DONE으로 바꾸지 않는다.
### 6.3 G7: 외부 종료 Gate
G7 내부 소스 계약은 완료됐고 `scripts/check-g7-external-proof.py`는 현재 의도적으로 exit 1이다.
네 artifact를 **같은 public host, 겹치는 50분 시간창**으로 수집해야 한다.
먼저 닫아야 할 운영·코드 선행조건이 있다.
- 공개 API는 여전히 119 paths 구버전이며 `/admin/voice-runtime`이 없고(404), `/voice/health`
`openai/gpt-4o-transcribe` + `openai/gpt-4o-mini-tts`다.
- local Whisper 9882와 MeloTTS 9883 listener는 아직 공개 호스트에 떠 있지 않다. 다만 current source의
`start-public-runtime.ps1`는 두 sidecar를 API보다 먼저 시작하고 `local_whisper/small/cpu-int8`,
`melotts/melotts-korean` exact provider/model·WS/HTTP readiness를 fail-closed로 확인하며, 기존 비정상 리스너를
임의 종료·재사용하지 않는다. Uvicorn도 `--ws websockets --ws-max-queue 4`로 고정했다. source 검증은
launcher/sidecar 80/80, API voice 71/71, PowerShell parse·Ruff·compile·diff-check PASS지만 공개 실행은 0회다.
- **외부 실행 전 코드 P0 3건은 완료:** (1) production 프로세스 exit 0은 canonical checker
`returncode == 0 && gate_closed == true`에 결속한다. (2) 실제 브라우저 Origin
`https://vignette.chanpaca.net`은 exact HTTPS allowlist로, API/WSS/admin/topology
`api-vignette.chanpaca.net`은 별도 transport host와 `wss`/`https` scheme으로 검증한다. (3) 실제 capture 기본·최소를
3,120초로 올리고 sampler를 ceil+terminal sample로 계산하며, checker가 voice/runtime/topology 공통 교집합
**3,000초 이상**을 강제한다. focused runner 37/37과 통합 G7 166/166을 통과했다.
- 현재 공개 topology는 Windows host Uvicorn + cloudflared다. 명시적 `windows_host` topology mode는 구현 완료됐다.
API/cloudflared의 PID·start time·executable name/SHA256·command-line SHA256·Git SHA·cwd를 pin하고, 각 sample 전후
identity와 Git HEAD drift를 검사한다. RSS/peak RSS/CPU/handles/threads, process TCP/established/listener,
API listen-port owner/conflict와 host TCP high-water도 수집한다. 기존 `linux_compose`의 api+caddy·fresh-container
규칙은 그대로 유지한다.
- Windows topology focused 검증은 G7 runner/checker/topology 86/86, Ruff, `py_compile`, `git diff --check`를 통과했고 실제
read-only adapter가 API 8001 owner=1/conflict=0, cloudflared established TCP>0를 읽었다. raw command line이나
endpoint는 저장하지 않았다.
- **남은 provenance blocker는 배포/실증:** 현재 API cwd는
`D:\workspace\vignette-outcome-g7-release-20260807\apps\api`이고 그 release 디렉터리에는 `.git`이 없다. 소스의
`-RequireFreshPublicProvenance` 모드는 detached-clean commit/tree와 Python/cloudflared/config SHA를 mutation 전에
검증하고, legacy API와 exact-config cloudflared를 bounded stop한 뒤 pinned release cwd에서 새 PID로 교체한다.
PID/start/exe SHA/command SHA/실제 cwd를 재검증한 safe receipt에는 raw command line·config 내용을 남기지 않는다.
topology artifact도 같은 commit/tree, runner/collector/checker SHA와 `psutil==6.1.1`을 매 sample 전후 결속한다.
이 코드는 atomic receipt contract 18/18을 포함한 G7 통합 166/166으로 GREEN이지만 **현재 public에는 아직 실행하지 않았다.**
clean commit 전 legacy cwd/Git pin을 약화하거나 기존 PID를 증거로 재사용하지 않는다.
- watchdog·로그온 boot의 detached-clean source pin 코드는 완료됐지만 실제 task action은 아직 legacy shared
source다. clean commit 뒤 `docs/ops/public-runtime-watchdog.md` 순서대로 같은 stable release root에 두 task를
재등록하고 commit/tree/script hash marker와 `LastTaskResult=0`을 확인한다. 그 전에는 current
`LastTaskResult=1`을 실패가 아니라 안전 차단으로 유지하고, 단일 mutation owner가 public 배포 시간창을 소유한다.
- 운영 Python에 `psutil`이 없으면 collector는 `command_unavailable:psutil`로 fail-closed한다. current
`apps/api/requirements.txt`와 명시적 Python 3.11 모두 `psutil==6.1.1`로 고정됐다.
- 현재 운영 STT 모델은 이 호스트에서 실측된 CPU int8 `small`로 고정한다. cuDNN 9가 설치되고 별도 성능·정확도
gate를 통과하기 전까지 launcher·API runtime metadata·50분 runner/checker expected model을 모두 `small`로 유지한다.
```powershell
& $py -X utf8 -B scripts/check-g7-external-proof.py `
--voice-soak <soak.json> --runtime <runtime.json> `
--topology <topology.json> --human-voice-gain <pack.json>
```
1. `scripts/soak-public-voice-websocket.py` v4 — 운영 기본값 `local_whisper`/`melotts`의 ready metadata가
실제 provider/model과 정확히 일치하는 authenticated public WSS,
**명시 동의 물리 마이크** 50분 양방향 `passed`. `--confirm-physical-capture` 없이는 장치 열거·캡처를 하지 않는다.
2. `scripts/capture-g7-runtime-evidence.py` — 같은 시간창의 관리자 endpoint worker/Uvicorn queue high-water.
3. `scripts/capture-g7-topology-evidence.py` — 같은 host·exact image의 50분 cgroup/proc/Docker/TCP high-water.
4. 독립 blind human voice-gain pack — held-out 30명 외 calibration split 참가자 포함 총 최소 31명 /
held-out 50회기 / 150 paired axis / blind evaluator 2인 /
ICC(A,1) ≥ 0.75 · κ ≥ 0.70 · gain ≥ 0.01 · participant-cluster bootstrap 10,000회 95% CI lower > 0.
준비 상세는 `docs/ops/outcome-os-g7-external-proof-readiness-2026-08-07.md`.
G7 Windows topology 회귀 명령:
```powershell
Set-Location D:\workspace\vignette
$env:PYTHONPATH='D:\workspace\vignette\apps\api'
& $py -X utf8 -m unittest scripts/test_g7_external_proof.py scripts/test_g7_topology_evidence.py `
scripts/test_g7_runtime_evidence.py scripts/test_g7_external_voice_soak.py `
scripts/test_run_g7_external_proof_window.py apps/api/app/test_g7_voice_gain_evidence.py
& $ruff check scripts/capture-g7-topology-evidence.py scripts/check-g7-external-proof.py `
scripts/run-g7-external-proof-window.py scripts/test_g7_topology_evidence.py `
scripts/test_g7_external_proof.py scripts/test_run_g7_external_proof_window.py
```
**사용자에게 받아야 하는 것 (코드로 대체 불가):**
- authenticated learner/admin session 또는 안전한 `storageState` 생성 협조
- 물리 마이크 50분 실행에 대한 **명시적 동의**
- 실제 참가자·독립 평가자 운영 승인
합성 label이나 무동의 mic probe로 대체하지 않는다. `check-g7-external-proof.py` exit 0 전에는 G7 메인 상태를
DONE으로 바꾸지 않는다.
## 7. 학생 자기주도 학습 폐루프
```text
학습자 홈 추천 → 새 회기/사전 설정 → 실제 Session → 종료 리뷰 → G4 처방 또는 G5 전이 의도
→ 별도 재연습 회기 → completed-session observer → 실제 G4 attempt / G5 transfer execution
→ read-model reload → before/after·진행도·멱등 재확인
```
실제 DB-backed browser 증거: `apps/web/e2e/returned-practice-db-closed-loop.spec.ts` +
`apps/web/e2e/harness/prepare-returned-practice-db.py`, desktop 2/2 + mobile 2/2, route mock 0,
visible raw UUID 0, getUserMedia/enumerateDevices 0, horizontal overflow 0.
이 harness는 **disposable clone**(전용 DB container + 전용 API 포트 + 전용 vite 포트)에서만 실행한다.
공개 런타임 포트를 재사용하지 않는다. `--api-base-url`, `--database-url`, `--practice-internal-token`,
`--transfer-internal-token`이 필요하고 API health의 `db`·`engine`이 모두 true여야 한다.
2026-08-07 재실행 절차(그대로 재현 가능):
1. `docker run -d --name vignette-g8-e2e-db-<날짜> -p 127.0.0.1:55439:5432` + `infra/db/init` 마운트,
`POSTGRES_USER=vignette_owner` / `APP_DB_USER=vignette_app`. `app.ci_regression_dag_node`가 생기면 준비 완료.
2. 별도 엔진 게이트웨이를 **새 포트**(예: 9199)에 띄운다. 상주 게이트웨이의 claude 세션이 죽어 있으면
`engine=false`가 되므로 공개용 9099를 재사용하지 않는다.
3. uvicorn API를 8021에, vite를 5199에 띄우고 `ENGINE_URL`을 2번 게이트웨이로 지정한다.
4. harness 실행 → `E2E_RETURNED_PRACTICE_DB_CLOSED_LOOP=1` + `E2E_RETURNED_PRACTICE_FIXTURE`로 spec 실행.
5. 결과 기준: desktop 2/2 + mobile 2/2 = **4 passed**. 끝나면 컨테이너·프로세스를 모두 정리한다.
G4/G5 핵심 production bug 수정은 보존한다.
- G4 runtime SQL `digest(...)``app.digest(...)`
- G5 JSONB bind는 `json.dumps` string이 아니라 dict/list object 전달
- route는 nested Pydantic suite를 `body.model_dump()` dict로 깨지 않고 typed object로 전달
## 8. 최종 검증 명령과 현재 기준선
```powershell
$py = 'C:\Users\encep\AppData\Local\Programs\Python\Python311\python.exe'
$ruff = 'C:\Users\encep\AppData\Local\hermes\hermes-agent\venv\Scripts\ruff.exe'
```
### Backend full — 기준 API **921 passed**, gateway **58 passed**
```powershell
Set-Location D:\workspace\vignette\apps\api
& $py -X utf8 -B -m pytest -p no:cacheprovider app -q
& $py -X utf8 -B -m pytest -p no:cacheprovider engine_gateway -q
```
### G8 executor/probe/helper/release governance — 기준 28 / 11 / 37 / 30
```powershell
Set-Location D:\workspace\vignette
& $py -X utf8 -B -m unittest scripts/test_serve_nas_preview_rollback_executor.py
& $py -X utf8 -B -m unittest scripts/test_probe_nas_preview_g8_rollback.py
& $py -X utf8 -B -m unittest scripts/test_launch_nas_preview_g8_helpers.py
& $py -X utf8 -B -m pytest -p no:cacheprovider scripts/test_outcome_os_release_agent.py -q
& $ruff check scripts/serve-nas-preview-rollback-executor.py scripts/test_serve_nas_preview_rollback_executor.py scripts/probe-nas-preview-g8-rollback.py scripts/test_probe_nas_preview_g8_rollback.py scripts/launch-nas-preview-g8-helpers.py scripts/test_launch_nas_preview_g8_helpers.py scripts/run-outcome-os-release-agent.py scripts/test_outcome_os_release_agent.py
```
### Web contract/type/build
```powershell
Set-Location D:\workspace\vignette\apps\web
npm run check:api-types
npm run typecheck
npm run build
```
### 현재 HEAD 학생 UX·접근성 focused — 기준 **20/20**
2026-08-09 읽기 전용 재감사에서 `check:api-types`·typecheck·build와 아래 fixture-only 묶음이 desktop/mobile
20/20을 통과했다: Alliance Pulse 8, 자기주도 폐루프 2, insecure-origin UUID 6, pre/mid 자기점검 2,
시간만료 경고·종료 모달 2. 390×844·320×568 overflow 0, tab 키보드, Enter CTA, 44px 조작부,
4.5:1 대비, raw UUID/theory key 비노출, 동의 전 `getUserMedia`/`enumerateDevices` 0도 확인했다.
추가 회귀에서 새 음성 동의 modal을 실제로 수락한 뒤 위기 음성 종료까지 가는 desktop/mobile, 모바일 review
filter 44px 계약을 desktop/mobile로 검증해 4/4 통과했다. dashboard의 file-origin은 spec-relative repo root로
고정했고, session-review fixture 소유권과 viewport assertion, admin live snapshot race를 교정했다. 16:49 단일 실행은
**120/120 PASS(92.0s)**였지만, 그 뒤 dashboard 변경분은 dashboard 10/10으로만 따로 통과했다. 따라서 현 트리는
110 불변 + 10 최신 분할 GREEN이고, 최종 clean source에서 아래 exact 묶음을 다시 단일 실행해 HEAD/tree/archive
결속 뒤 120/120을 요구한다.
#### current-source fixture exact 120 — clean HEAD 재현 명령
정확한 수집 구성은 `dev-dashboard` 10 + `session-mvp` 24 + `session-review` 24 + `learner` 14 +
`session-layout` 8 + `admin` 40 = desktop/mobile 120이다. `127.0.0.1:8000`은 반드시 복구 dump에서 만든
**disposable API/DB/engine 전용 스택**이어야 한다. 이 묶음은 dev-login·세션 쓰기를 포함할 수 있으므로 public
8001, 복구 원본 55432, candidate DB, NAS DB에 연결하지 않는다.
```powershell
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
$OutputEncoding = [Text.UTF8Encoding]::new($false)
$cleanRoot = 'D:\workspace\vignette-clean-<NEW_SHA>'
$cleanWeb = Join-Path $cleanRoot 'apps\web'
$tempRoot = Join-Path $env:TEMP 'vignette-clean-fixture-120'
$tempWeb = Join-Path $tempRoot 'apps\web'
$tempEvidence = Join-Path $tempRoot 'docs\ops\evidence'
if (Test-Path -LiteralPath $tempRoot) { throw "기존 TEMP와 충돌: $tempRoot" }
$pw = Join-Path $cleanWeb 'node_modules\.bin\playwright.cmd'
if (-not (Test-Path -LiteralPath $pw -PathType Leaf)) {
throw 'clean worktree에서 npm ci --ignore-scripts를 먼저 실행해 node_modules를 준비할 것'
}
$health = Invoke-RestMethod -Uri 'http://127.0.0.1:8000/health' -TimeoutSec 5
if ($health.status -ne 'ok' -or -not $health.db -or -not $health.engine) {
throw 'disposable API8000의 db/engine ready가 아님'
}
New-Item -ItemType Directory -Path $tempWeb -Force | Out-Null
New-Item -ItemType Directory -Path $tempEvidence -Force | Out-Null
$config = Join-Path $cleanWeb 'playwright.config.ts'
$files = @(
'e2e/dev-dashboard.spec.ts',
'e2e/session-mvp.spec.ts',
'e2e/session-review.spec.ts',
'e2e/learner.spec.ts',
'e2e/session-layout.spec.ts',
'e2e/admin.spec.ts'
)
$env:PLAYWRIGHT_PORT = '15374'
$env:VITE_API_PROXY_TARGET = 'http://127.0.0.1:8000'
Remove-Item Env:PLAYWRIGHT_BASE_URL -ErrorAction SilentlyContinue
Remove-Item Env:PLAYWRIGHT_SKIP_WEB_SERVER -ErrorAction SilentlyContinue
$code = 99
try {
Push-Location $tempWeb
try {
& $pw test @files ("--config={0}" -f $config) `
--project=chromium-desktop --project=chromium-mobile `
--workers=4 --reporter=line ("--output={0}" -f (Join-Path $tempRoot 'pw-results'))
$code = $LASTEXITCODE
} finally {
Pop-Location
}
} finally {
$listeners = @(Get-NetTCPConnection -State Listen -LocalPort 15374 -ErrorAction SilentlyContinue)
$listenerError = $null
if ($listeners.Count -ne 0) { $listenerError = "Playwright Vite listener 잔존: $($listeners.OwningProcess -join ',')" }
if (Test-Path -LiteralPath $tempRoot) {
$resolved = (Resolve-Path -LiteralPath $tempRoot).Path
if ($resolved -ne $tempRoot) { throw "unexpected TEMP target: $resolved" }
[IO.Directory]::Delete($tempRoot, $true)
}
if ($listenerError) { throw $listenerError }
}
if ($code -ne 0) { throw "clean fixture 120 failed: $code" }
```
과거 API-ready 없이 돌린 별도 Vite-only 혼합 실행의 `session-layout` 8건은 DB persona가 없어 제품 assertion 전에
`seed_fallback/degraded`로 중단됐다. 위 exact 120 명령은 이 사각지대를 닫기 위해 disposable API의 db/engine health를
선행 강제한다. 최종 완료 때는 `session-layout` 8/8과 returned-practice DB closed-loop 4/4를 모두 다시 통과시킨다.
### Dashboard SSOT + E2E — 기준 SSOT PASS(FAIL 0), unit 5/5, dashboard 10/10
```powershell
Set-Location D:\workspace\vignette
& $py -X utf8 -B scripts/check-dev-dashboard-ssot.py
& $py -X utf8 -B -m pytest -p no:cacheprovider scripts/test_dev_dashboard_ssot.py -q
Set-Location D:\workspace\vignette\apps\web
.\node_modules\.bin\playwright.cmd test e2e/dev-dashboard.spec.ts --project=chromium-desktop --project=chromium-mobile --workers=1 --reporter=line
```
status count를 바꾸면 `scripts/check-dev-dashboard-ssot.py``EXPECTED_STATUS_COUNTS`
`apps/web/e2e/dev-dashboard.spec.ts``metrics.done`/`metrics.doing`**함께** 갱신한다.
### 격리 NAS 런타임 회기 E2E
```powershell
Set-Location D:\workspace\vignette\apps\web
$env:PLAYWRIGHT_BASE_URL='http://100.116.83.60:8088'
$env:PLAYWRIGHT_SKIP_WEB_SERVER='1'
node.exe .\node_modules\@playwright\test\cli.js test e2e/session-layout.spec.ts e2e/session-persistence.spec.ts `
e2e/self-directed-learning-loop.spec.ts e2e/alliance-pulse.spec.ts e2e/outcome-trajectory.spec.ts `
e2e/rupture-repair.spec.ts e2e/deliberate-practice.spec.ts e2e/calibration-transfer.spec.ts `
e2e/supervision-research.spec.ts e2e/multimodal-alliance.spec.ts e2e/continuous-improvement-admin.spec.ts `
--project=chromium-desktop --project=chromium-mobile --project=chromium-single-run --workers=1 --reporter=line
```
전체 Playwright inventory는 614 tests / 44 files다. 실행 환경/API/DB를 정확히 맞추지 않고 fixture failure를
제품 failure로 오인하지 않는다. 같은 blocker가 두 번 반복되면 전체 재시도 대신 원인·증거·수정 계획을 먼저 보고한다.
## 9. 핵심 변경 파일
- `scripts/launch-nas-preview-g8-helpers.py` · `scripts/test_launch_nas_preview_g8_helpers.py` (신규, Gate6 계약)
- `scripts/serve-nas-preview-rollback-executor.py` · `scripts/test_serve_nas_preview_rollback_executor.py`
- `scripts/probe-nas-preview-g8-rollback.py` · `scripts/test_probe_nas_preview_g8_rollback.py`
- `scripts/run-outcome-os-release-agent.py` · `scripts/test_outcome_os_release_agent.py`
- `scripts/check-dev-dashboard-ssot.py` · `scripts/test_dev_dashboard_ssot.py`
- `scripts/check-g7-external-proof.py` · `scripts/soak-public-voice-websocket.py` ·
`scripts/capture-g7-runtime-evidence.py` · `scripts/capture-g7-topology-evidence.py` ·
`scripts/run-g7-external-proof-window.py`와 대응 G7 테스트 3개
- `apps/web/src/lib/uuid.ts` · `apps/web/e2e/insecure-context-uuid.spec.ts` (신규, 비-secure origin 결함 수정)
- `apps/api/app/session_read_model.py` · `apps/api/app/services/session_metrics.py` ·
`apps/api/app/test_session_read_model.py` · `apps/web/e2e/session-persistence.spec.ts` (UTC/KST P0와 실DB browser 회귀)
- `apps/web/index.html` · `apps/web/e2e/self-directed-learning-loop.spec.ts` (SPA route 전환 중 boot diagnostic 회복)
- `apps/web/e2e/admin.spec.ts` · `apps/web/e2e/dev-dashboard.spec.ts` ·
`apps/web/e2e/session-review-fixture.ts` · `apps/web/e2e/session-review.spec.ts`
- `scripts/watch-public-runtime.ps1` · `scripts/boot-public-runtime.ps1` ·
`scripts/install-public-runtime-task.ps1` · `scripts/register-boot-task.ps1` ·
`scripts/watch-public-runtime-hidden.vbs` · `scripts/test_public_runtime_watchdog_provenance.py`
- `apps/web/e2e/returned-practice-db-closed-loop.spec.ts` ·
`apps/web/e2e/harness/prepare-returned-practice-db.py`
- `docs/dev_dashboard.html` · `docs/TODO.md` · `docs/README.md` · `docs/ops/backlog-2026-06-26.md`
- `docs/ops/nas-preview-g8-rollback-proof-runbook.md` ·
`docs/ops/evidence/nas-preview-g8-actual-rollback-2026-08-07.json` ·
`docs/ops/nas-preview-deployment-evidence-2026-08-07.md`
## 10. 최종 원칙
- source-only PASS를 runtime DONE으로 부르지 않는다.
- health만 보고 배포 완료라고 하지 않는다. auth, OpenAPI, assets, browser SSE→DB review를 함께 본다.
- synthetic control plane을 실제 NAS rollback으로 과장하지 않는다.
- G7 물리 마이크·사람 평가를 무동의/합성 데이터로 대체하지 않는다.
- current/previous images와 DB backup을 확인하기 전 destructive operation을 실행하지 않는다.
- 성공보다 정직한 fail-closed 증거가 우선이다.
## 11. 권장 멀티에이전트 실행 구조
사람 개발자 작업계획은 만들지 않는다. 구현·테스트·배포·증거 동기화는 에이전트가 수행하고, 사람에게는 코드로
대체 불가능한 동의·인증 세션·실제 참가자/평가자 승인만 요청한다. 동시 작업은 최대 세 하위 lane으로 나누되,
NAS/public/DB mutation owner는 항상 한 에이전트만 둔다.
1. **G8 release lane:** clean-head release mode 구현·unit·dry-run·NAS preview 배포·NAS-origin E2E. 이 lane만 NAS mutation.
2. **G7 proof lane:** current public route/config 감사, local provider readiness, `--rehearse`, 네 artifact 수집 준비.
실제 마이크는 사용자 명시 동의 전 0회, public mutation은 G8 lane과 시간 겹치지 않게 단일 owner에게 인계.
3. **E2E/SSOT lane:** local fixture, disposable DB browser, visual/accessibility, dashboard/TODO/backlog 정합을 읽기 전용으로 감사.
루트 에이전트는 각 lane의 증거 SHA와 실패 원인을 합쳐 게이트를 판정한다. 같은 blocker가 두 번 반복되면 세 번째
재시도 전에 원인·증거·수정 계획을 사용자에게 보고한다.
## 12. 새 에이전트에 그대로 줄 시작 프롬프트
```text
D:\workspace\vignette의 docs/HANDOFF.md를 인수인계 SSOT로 읽고, AGENTS.md → docs/README.md →
docs/dev_dashboard.html → docs/TODO.md → docs/ops/backlog-2026-06-26.md 순서로 현재 상태를 재확인해.
현재 master는 HEAD 94c681d…7fb이고 origin/master보다 2 commit 앞서며, timestamp·voice provider/model,
G7 topology·public launcher·G8 clean-head·SSOT가
아직 tracked dirty다. 기존 D:\workspace\vignette-clean-head-94c681d와 g8-focus-head-94c681d.tar는 이 최신 변경을
포함하지 않으므로 배포 입력으로 사용하지 마. 먼저 git status/diff와 각 lane의 focused test를 재확인해.
최종 목표는 G0~G8 아홉 목표를 코드-only가 아니라 실제 runtime 증거로 모두 닫는 것이다. G0~G6은 현재 내부 DONE
증거를 보존하되 새 clean HEAD 전체 회귀로 재확인해. G8 rollback receipt와 clean-head controller는 구현됐지만
2026-08-09 execute 두 번은 모두 NAS mutation 전에 fail-closed됐다. docs/HANDOFF.md §6.1의 naive
session timestamp 버그는 UTC/KST 계약과 focused disposable DB/API/browser E2E로 GREEN이다. 테스트 강제 클릭 우회는
없다. 사용자 승인을 받아 이 변경을 새 clean commit으로 만들고 HEAD/tree/archive를 다시 결속해 격리 NAS preview에
배포한 다음 실제 NAS HTTP origin의 전체 회기 E2E를 0 failure로 통과시켜. G7은 내부 구현만 DONE이다. current source 공개 배포,
local_whisper/melotts ready, authenticated public WSS, 사용자의 명시 동의를 받은 물리 마이크 50분 soak,
같은 시간창의 runtime/topology high-water, 독립 human voice-gain pack까지 모아 canonical checker exit 0을 만들어야 한다.
G7 runner exit의 checker exit 0/gate_closed 결속, browser Origin과 API/WSS host 분리, 3,120초 capture와
3,000초 공통 overlap, detached-clean HEAD/tree, runner/collector/checker SHA와 exact psutil pin은 소스에서 완료됐다.
API/cloudflared는 legacy PID를 재사용하지 말고 `-RequireFreshPublicProvenance`로 pinned release cwd에서 강제
재시작해 새 PID/start/exe/command SHA/cwd safe receipt를 결속해.
G7 runner/checker/topology는 86/86, public launcher/sidecar 계약은 80/80, 통합은 166/166, API voice 통합은 71/71로 구현됐지만
현재 public API의 Git 없는 냉동 release cwd는 provenance gate를
통과하지 못한다. current committed repo 기반 launcher/runtime으로 정렬하거나 동등하게 강한 provenance를 구현하고,
cwd/Git pin이나 기존 Linux Compose gate를 약화하지 마. 합성 증거로 G7을 DONE 처리하지 마.
public task source-pin 코드는 구현됐다. watchdog·로그온 boot는 detached clean release와 exact Git commit/tree,
watchdog|boot/start script SHA가 없으면 mutation 전에 실패한다. 하지만 실제 두 Scheduled Task action은 아직 shared
worktree와 legacy 인자를 가리키며, 5분 watchdog은 최신 실행에서 pin 인자 부재로 `LastTaskResult=1`이다. public은
현재 health ok지만 자동복구는 의도적으로 fail-closed다. clean commit 뒤 동일 stable release root로 두 task를
재등록하고 action pin·watchdog result 0·로그온/reboot smoke를 증명한 뒤에만 runtime DONE으로 바꿔.
NAS 17:07 기준선은 active 6030a677…c611, API/Web 52e0…8b2d/6fdb…f215, previous images 보존,
Compose 4개 running+unless-stopped, volume vignette-preview-20260807_pgdata, health/routes/assets GREEN, helper/listener 0이다.
release agent는 DB dump/restore를 지원하지 않으므로 execute 직전 fresh custom dump SHA/size/TOC와 DB identity를
별도로 결속해. 자동 rollback은 image/Compose/active-state뿐이고 migration/data restore는 별도 owner 승인이야.
remote release root/Compose mode 0777 P1도 최소권한 preflight로 닫기 전에는 execute하지 마.
학생 자기주도 폐루프는 홈 추천→회기→리뷰→G4 처방/G5 전이→별도 재연습→completed-session observer→
actual attempt/execution→read-model reload→before/after·멱등까지 desktop/mobile 실제 DB/browser로 검증해.
최종 검증은 API full, gateway full, schema/provenance, Web API types/typecheck/build, fixture E2E, disposable DB E2E,
NAS-origin E2E, visual/accessibility, auth/OpenAPI/assets/SSE→DB review를 포함해야 한다.
독립 작업은 서브에이전트로 병렬화하되 NAS/public/DB mutation은 단일 owner만 수행해. 공개 DB 복구 volume과
pre-recovery container/dumps, NAS named volume과 previous images는 삭제하지 마. 비밀/PII/raw UUID를 증거에 남기지 마.
과거 로컬 recovery dump 경로는 인계 시점에 보이지 않으므로 삭제로 단정하거나 그 파일을 전제로 복원하지 말고,
off-host/NAS 보관 위치와 새 backup 목적지를 먼저 확인해.
source-only PASS나 health-only를 DONE으로 부르지 말고, 대시보드·TODO·backlog·HANDOFF를 마지막 증거와 함께 동기화해.
모든 게이트가 실제로 닫히기 전에는 goal complete를 선언하지 마.
```