@kaydash9999/banker-plugins 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +4 -3
- package/codex/manifest.json +1 -0
- package/codex/transform-matrix.md +1 -0
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +1 -0
- package/skills/3d-intro-build/references/azure-adapter.mjs +54 -27
- package/skills/3d-intro-build/references/video-pool.mjs +11 -6
- package/skills/3d-intro-setup/references/azure-adapter.mjs +54 -27
- package/skills/3d-intro-setup/references/video-pool.mjs +11 -6
- package/skills/make-evidence/SKILL.md +182 -0
- package/skills/motion-sites/SKILL.md +9 -1
- package/skills/motion-sites/scripts/motion-sites.mjs +20 -3
- package/skills/omc-patch/scripts/omc-patch.mjs +74 -16
- package/skills/ralph-qa/SKILL.md +1 -0
- package/skills/ralph-qa/references/seat-run.mjs +21 -4
- package/skills/ralph-qa/references/verifier-probe.mjs +35 -13
- package/skills/remains/SKILL.md +1 -1
- package/skills/remains/scripts/boxes.mjs +31 -12
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: make-evidence
|
|
3
|
+
description: "구현한 서비스의 증거 문서 여섯(optimize, verify, flow, scale, decisions, trace)을 실제 실행으로 만들어 docs/evidence/ 에 커밋. 수치는 실행 로그에서만 옮기고, 문서마다 실행 명령과 로그 경로를 적음. 판정기는 구현을 보지 않은 새 맥락 하위 에이전트가 씀. 미구현은 숨기지 않음. 'make-evidence'/'증거 문서'/'evidence 만들어'/'검증 근거 문서' 시 사용."
|
|
4
|
+
argument-hint: "[--host <host>] [--port <port>] [--only <문서,...>] [--no-commit]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# make-evidence: 실행으로 만드는 증거 문서 여섯
|
|
8
|
+
|
|
9
|
+
구현한 서비스가 README 의 요구를 지키는지, 실제로 실행한 결과로 보이는 문서 여섯을 만든다.
|
|
10
|
+
문서는 `docs/evidence/` 에 쓰고 커밋한다. 답변은 한글.
|
|
11
|
+
|
|
12
|
+
## 원칙
|
|
13
|
+
|
|
14
|
+
- **수치는 손으로 쓰지 않는다.** 시간, 목적값, 차이(%), 통과 수, 건수는 실행 로그에서만 옮긴다. 로그에 없는 수는 문서에 쓰지 않는다.
|
|
15
|
+
- **문서마다 실행 명령과 로그 경로를 적는다.** 읽는 사람이 같은 명령을 다시 돌려 같은 로그를 얻을 수 있어야 한다.
|
|
16
|
+
- **실행하지 못한 것은 실행하지 못했다고 쓴다.** 서버가 안 뜨거나 도구가 없으면 그 칸은 `미검증` 과 까닭이다. 추정값이나 예상값으로 채우지 않는다.
|
|
17
|
+
- **미구현을 숨기지 않는다.** README 요구 가운데 구현하지 않은 것은 `trace.md` 에 `미구현` 과 이유로 남긴다.
|
|
18
|
+
- 문서에 비밀값을 넣지 않는다. 로그에 토큰, 비밀번호, 키가 찍혔으면 커밋 전에 값을 `***` 로 가리고, 가렸다는 사실을 그 문서에 적는다.
|
|
19
|
+
|
|
20
|
+
## 인자
|
|
21
|
+
|
|
22
|
+
- `--host <host>`, `--port <port>`: `flow.md` 의 종단 시험이 요청을 보낼 서버. 없으면 README, `.env.example`, 서비스 시작 스크립트에서 찾고, 그래도 없으면 사용자에게 묻는다. 찾은 값과 그 출처를 `flow.md` 에 적는다.
|
|
23
|
+
- `--only <문서,...>`: 일부만 만든다(예: `--only scale,trace`). 이름은 `optimize`, `verify`, `flow`, `scale`, `decisions`, `trace`.
|
|
24
|
+
- `--no-commit`: 파일만 쓰고 커밋하지 않는다.
|
|
25
|
+
|
|
26
|
+
## 폴더
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
docs/evidence/
|
|
30
|
+
optimize.md verify.md flow.md scale.md decisions.md trace.md
|
|
31
|
+
logs/ 실행 로그. 문서는 이 파일들을 경로로 가리킨다
|
|
32
|
+
scripts/ 데이터 생성, 측정, 완전 탐색처럼 증거를 만드는 데 쓴 스크립트
|
|
33
|
+
tests/oracle/ 판정기와 그 시험 (verify.md)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- 로그 이름은 `<문서>-<무엇>-<YYYYMMDD-HHMMSS>.log` 다. 같은 시험을 다시 돌리면 새 파일을 만들고, 문서는 마지막 로그를 가리킨다.
|
|
37
|
+
- 로그는 명령의 표준 출력과 표준 오류를 함께 담고, 첫 줄에 실행한 명령, 마지막 줄에 종료 코드를 남긴다.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
run_log() { # run_log <로그 경로> <명령...>
|
|
41
|
+
mkdir -p "$(dirname "$1")"; f=$1; shift
|
|
42
|
+
{ echo "\$ $*"; "$@"; echo "exit=$?"; } > "$f" 2>&1
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 진행
|
|
47
|
+
|
|
48
|
+
먼저 저장소를 읽는다: README, 시작 스크립트, 시험 실행 방법, 최적화 코드, DB 스키마, 내보내기 코드.
|
|
49
|
+
문서 여섯은 아래 순서로 만든다. `verify.md` 의 판정기는 다른 문서보다 먼저 쓴다.
|
|
50
|
+
|
|
51
|
+
### 1. optimize.md
|
|
52
|
+
|
|
53
|
+
| 항목 | 내용 |
|
|
54
|
+
|---|---|
|
|
55
|
+
| 목적 함수 | 최소화 또는 최대화하는 값과 식 |
|
|
56
|
+
| 제약 | 제약마다 한 줄. 강한 제약과 약한 제약(벌점)을 나눔 |
|
|
57
|
+
| 알고리즘 이름 | 쓴 알고리즘의 이름(예: 정수 계획법, 담금질, 탐욕 + 지역 탐색). 라이브러리를 쓰면 그 이름과 판 |
|
|
58
|
+
| 핵심 함수 | `파일:줄` 로. 목적값 계산, 제약 검사, 탐색 본체를 각각 |
|
|
59
|
+
| 의사코드 | 10줄 |
|
|
60
|
+
|
|
61
|
+
- `파일:줄` 은 지금 커밋에서 실제로 그 줄을 열어 확인한 것만 쓴다. 문서 끝에 확인한 커밋 해시를 적는다.
|
|
62
|
+
- 이 문서에는 실행 수치가 없다. 실행 명령 칸에는 줄 번호를 확인한 명령(예: `grep -n 'def objective' src/opt.py`)과 그 출력 로그를 적는다.
|
|
63
|
+
|
|
64
|
+
### 2. verify.md
|
|
65
|
+
|
|
66
|
+
판정기(oracle)는 구현을 보지 않은 하위 에이전트가 쓴다. 구현을 본 맥락이 판정기를 쓰면 구현의 실수를 판정기도 따라 한다.
|
|
67
|
+
|
|
68
|
+
1. **담당자 사실을 모은다.** 담당자(사용자)가 준 업무 규칙, 수치 기준, 권한 규칙을 `docs/evidence/owner-facts.md` 에 그대로 옮긴다. 담당자 사실이 없으면 사용자에게 묻는다. 그래도 없으면 README 만으로 진행하고 그 사실을 `verify.md` 에 적는다.
|
|
69
|
+
2. **새 맥락 하위 에이전트를 띄운다.** 이 대화를 넘기지 않는다.
|
|
70
|
+
- Claude Code: Agent 도구를 `general-purpose` 로, `model` 없이 부른다.
|
|
71
|
+
- Codex: `spawn_agent` 를 `agent_type` 과 `model` 없이 부르고 대화를 넘기지 않는다(multi-agent v2 는 `fork_turns="none"`, v1 은 `fork_context` 를 주지 않음).
|
|
72
|
+
- 지시에 들어갈 것:
|
|
73
|
+
- 읽어도 되는 것은 `README.md` 와 `docs/evidence/owner-facts.md` 둘뿐이다.
|
|
74
|
+
- 구현 폴더와 기존 시험은 열지 않는다.
|
|
75
|
+
- 쓸 곳은 `tests/oracle/` 다.
|
|
76
|
+
- 판정기는 입력과 출력만 보고 세 가지를 판정한다.
|
|
77
|
+
- 제약을 모두 지키는가
|
|
78
|
+
- 보고된 목적값이 입력과 출력으로 다시 계산한 값과 같은가
|
|
79
|
+
- 권한 규칙을 지키는가
|
|
80
|
+
- 시험은 판정기를 쓴다.
|
|
81
|
+
- 끝에 읽은 파일 목록을 보고한다.
|
|
82
|
+
3. **읽은 파일 목록을 확인한다.** 목록에 두 파일 밖의 경로가 있으면 그 판정기는 버리고 다시 띄운다. 목록은 `verify.md` 에 옮긴다.
|
|
83
|
+
4. **판정기를 구현보다 먼저 커밋한다.** 커밋 메시지는 `test(oracle): ...` 다. 구현이 아직 없으면 이 커밋이 구현 커밋보다 앞선다. 구현이 이미 있으면 그 사실과 두 커밋의 순서(`git log --format='%h %ad %s' --date=iso -- tests/oracle <구현 경로>`)를 그대로 적는다. 순서를 꾸미지 않는다.
|
|
84
|
+
5. **판정기를 지금 구현에 돌린다.** 명령과 로그 경로, 통과와 실패 수를 로그에서 옮긴다.
|
|
85
|
+
6. **일부러 넣은 버그 셋을 판정기가 잡는지 본다.**
|
|
86
|
+
|
|
87
|
+
| 버그 | 넣는 방법의 예 |
|
|
88
|
+
|---|---|
|
|
89
|
+
| 제약 위반 | 제약 검사 한 줄을 건너뛰게 바꿈 |
|
|
90
|
+
| 목적값 오계산 | 목적값 합산에서 항 하나를 빼거나 부호를 바꿈 |
|
|
91
|
+
| 권한 우회 | 권한 확인을 늘 통과하게 바꿈 |
|
|
92
|
+
|
|
93
|
+
- 버그마다 차례로 진행한다.
|
|
94
|
+
1. 바꾼 것을 패치 파일로 남긴다: `docs/evidence/logs/verify-bug-<이름>.patch`
|
|
95
|
+
2. `git apply` 로 넣는다.
|
|
96
|
+
3. 판정기를 돌려 로그를 남긴다.
|
|
97
|
+
4. `git apply -R` 로 되돌린다.
|
|
98
|
+
5. `git status --short` 와 `git diff --stat` 이 빈 것을 로그로 남긴다.
|
|
99
|
+
- 표에는 버그마다 다섯 가지를 적는다: 패치 경로, 넣은 뒤 결과(잡음 또는 놓침), 로그 경로, 되돌린 명령, 되돌린 뒤 판정기 결과.
|
|
100
|
+
- 놓친 버그는 놓쳤다고 쓰고, 판정기의 빈틈을 한 줄로 적는다. 놓친 것을 잡게 하려고 판정기를 고치면 그 커밋과 재실행 로그를 더한다.
|
|
101
|
+
- 되돌리기가 실패하면 멈추고 사용자에게 알린다. 작업 트리를 지우거나 덮어쓰지 않는다.
|
|
102
|
+
|
|
103
|
+
### 3. flow.md
|
|
104
|
+
|
|
105
|
+
- 순서는 화면, API, 최적화, DB, 내보내기다.
|
|
106
|
+
- 단계마다 그 단계를 처리하는 `파일:줄` 을 적는다. 화면은 요청을 보내는 코드, API 는 라우트와 처리 함수, 최적화는 호출 지점, DB 는 쓰기와 읽기 쿼리, 내보내기는 파일을 만드는 함수다.
|
|
107
|
+
- **종단 시험**: `{host}:{port}` 에 떠 있는 서버에 실제 요청을 보내 다섯 단계를 끝까지 지난다.
|
|
108
|
+
- 스크립트는 `docs/evidence/scripts/e2e-flow.*` 다.
|
|
109
|
+
- 서버가 떠 있지 않으면 저장소의 시작 방법으로 띄운다. 띄운 명령과 그 로그를 적는다.
|
|
110
|
+
- 화면 단계는 브라우저 도구(Playwright 등)가 있으면 실제로 누르고, 없으면 화면 URL 의 응답과 화면이 부르는 API 요청을 그대로 보낸다. 어느 쪽인지 적는다.
|
|
111
|
+
- 단계마다 다섯 가지를 로그에 남긴다: 요청, 응답 코드, 걸린 시간, DB 에 생긴 행의 확인 쿼리 결과, 내보낸 파일의 이름과 크기.
|
|
112
|
+
- 서버에 닿지 못하면 종단 시험 칸은 `미검증` 과 오류 문구다. 단위 시험으로 대신하지 않는다.
|
|
113
|
+
- 운영 서버나 남의 서버에는 보내지 않는다. 대상이 로컬이나 개발 서버가 아니면 사용자에게 먼저 확인한다.
|
|
114
|
+
|
|
115
|
+
### 4. scale.md
|
|
116
|
+
|
|
117
|
+
- **데이터 1배, 5배, 10배**: 기준 데이터(README 의 예시나 시험 고정물)를 늘리는 스크립트 `docs/evidence/scripts/gen-scale.*` 로 만든다. 같은 시드로 다시 만들 수 있게 시드를 고정하고 적는다.
|
|
118
|
+
- 배수마다 최적화를 돌려 걸린 시간과 목적값을 잰다. 한 배수를 3회 돌려 각 값과 중앙값을 로그에 남긴다.
|
|
119
|
+
- **작은 사례 다섯**: 완전 탐색이 끝나는 크기로 다섯 개를 만들고, 완전 탐색 스크립트 `docs/evidence/scripts/brute-force.*` 로 정해를 구한다.
|
|
120
|
+
- 사례마다 구현의 목적값과 정해를 적는다.
|
|
121
|
+
- 차이(%)는 `|구현 - 정해| / |정해| x 100` 이다. 정해가 0 이면 차이 대신 두 값을 그대로 적는다.
|
|
122
|
+
- 차이도 스크립트가 계산해 로그에 찍는다.
|
|
123
|
+
- 표의 칸마다 로그 경로를 단다. 시간 제한에 걸린 배수는 `시간 초과(제한 N초)` 로 적는다.
|
|
124
|
+
|
|
125
|
+
### 5. decisions.md
|
|
126
|
+
|
|
127
|
+
결정마다 한 행이다.
|
|
128
|
+
|
|
129
|
+
| 결정 | 고른 것 | 버린 것 | 이유 | 확인하는 시험 |
|
|
130
|
+
|---|---|---|---|---|
|
|
131
|
+
|
|
132
|
+
- 결정은 코드와 커밋 기록, README, ADR 이 있으면 ADR 에서 찾는다. 찾은 곳을 적는다.
|
|
133
|
+
- 확인하는 시험은 실제로 있는 시험의 이름이다. 이름마다 통과 로그 경로를 단다. 그 결정을 확인하는 시험이 없으면 `없음` 으로 적는다. 이름을 지어내지 않는다.
|
|
134
|
+
|
|
135
|
+
### 6. trace.md
|
|
136
|
+
|
|
137
|
+
README 의 요구마다 한 행이다.
|
|
138
|
+
|
|
139
|
+
| 번호 | 요구 | 상태 | 시험 이름 | 로그 경로 |
|
|
140
|
+
|---|---|---|---|---|
|
|
141
|
+
|
|
142
|
+
- 번호는 README 에 나온 순서로 `R1`, `R2` 다. 요구 칸에는 README 문장을 줄이지 않고 옮긴다.
|
|
143
|
+
- 상태는 셋 가운데 하나다.
|
|
144
|
+
- `완료`: 그 요구를 확인하는 시험이 있고 통과함
|
|
145
|
+
- `부분`: 일부만 구현했거나 시험이 일부만 확인함. 빠진 부분을 적음
|
|
146
|
+
- `미구현`: 이유를 적음
|
|
147
|
+
- 시험이 실패하면 `완료` 로 쓰지 않는다. 시험이 없으면 `완료` 가 아니라 `부분` 이고, 시험이 없다고 적는다.
|
|
148
|
+
- 마지막에 상태별 개수를 적는다. 개수도 표에서 센 것이다.
|
|
149
|
+
|
|
150
|
+
## 수치 대조
|
|
151
|
+
|
|
152
|
+
문서를 다 쓴 뒤, 문서에 적은 수마다 그 칸이 가리키는 로그에 같은 수가 있는지 확인한다.
|
|
153
|
+
|
|
154
|
+
- 문서의 숫자(시간, 목적값, 차이, 개수)를 하나씩 뽑아 가리킨 로그에서 찾는다(`grep -F`).
|
|
155
|
+
- 로그에서 찾지 못한 수는 지우거나, 그 수를 찍는 명령을 다시 돌려 로그를 만든다.
|
|
156
|
+
- 대조 결과(확인한 수의 개수, 못 찾은 수)를 각 문서 끝의 `수치 대조` 줄에 적는다.
|
|
157
|
+
|
|
158
|
+
## 커밋
|
|
159
|
+
|
|
160
|
+
- 커밋 대상은 `docs/evidence/` 와 `tests/oracle/` 뿐이다. 다른 경로는 넣지 않는다.
|
|
161
|
+
- 증거를 만들다 구현을 고쳤다면 그 수정은 따로 커밋하고 사용자에게 알린다.
|
|
162
|
+
- 판정기 커밋은 2번의 4단계에서 이미 했다.
|
|
163
|
+
- 커밋 전에 비밀값을 찾는다. 걸린 값은 가린다. 커밋 메시지는 `docs(evidence): ...` 다.
|
|
164
|
+
- push 하지 않는다. 사용자가 요청할 때만 한다.
|
|
165
|
+
- `--no-commit` 이면 쓸 파일 목록만 보이고 커밋하지 않는다.
|
|
166
|
+
|
|
167
|
+
## 런타임
|
|
168
|
+
|
|
169
|
+
- Claude Code: `/banker:make-evidence`. 하위 에이전트는 Agent 도구다.
|
|
170
|
+
- Codex: npm 길 `$banker-make-evidence`, 플러그인 길 `$banker:make-evidence`. 하위 에이전트는 `spawn_agent` 다.
|
|
171
|
+
- 기본 샌드박스가 `.git` 쓰기를 막으면 커밋 명령에 `sandbox_permissions: "require_escalated"` 를 청한다.
|
|
172
|
+
- 로컬이 아닌 서버에 보내는 종단 시험은 네트워크 승인이 필요하다.
|
|
173
|
+
|
|
174
|
+
## 보고
|
|
175
|
+
|
|
176
|
+
끝나면 표 하나로 알린다.
|
|
177
|
+
|
|
178
|
+
| 문서 | 상태 | 로그 수 | 미검증이나 미구현 |
|
|
179
|
+
|---|---|---|---|
|
|
180
|
+
|
|
181
|
+
- 상태는 `작성`, `일부 미검증`, `작성 안 함(까닭)` 가운데 하나다.
|
|
182
|
+
- 커밋 해시를 적는다. push 는 하지 않았다고 적는다.
|
|
@@ -29,7 +29,7 @@ banker 는 원본을 복사하지 않는다. 원본의 예시(`examples/`)와
|
|
|
29
29
|
|---|---|
|
|
30
30
|
| `skillFile` | 설치된 원본 `SKILL.md` 경로. 없거나 꺼져 있으면 `null` |
|
|
31
31
|
| `disabled` | 설치는 됐지만 꺼져 있음 |
|
|
32
|
-
| `python.bin` | Playwright 를 쓸 Python |
|
|
32
|
+
| `python.bin` | Playwright 를 쓸 Python. 찾는 차례는 `MOTION_SITES_PYTHON`, 캐시 폴더의 가상 환경, PATH 의 `python3`, `python`(Windows 는 `py -3`, `python`). `MOTION_SITES_PYTHON` 이 있으면 그것과 캐시 폴더의 가상 환경만 보고, 지정한 것이 실행되지 않으면 다른 Python 으로 넘어가지 않는다 |
|
|
33
33
|
| `python.playwright`, `python.chromium` | Playwright 와 Chromium 이 있는지 |
|
|
34
34
|
| `ready` | 셋 다 있으면 `true`(종료 0) |
|
|
35
35
|
|
|
@@ -49,6 +49,14 @@ banker 는 원본을 복사하지 않는다. 원본의 예시(`examples/`)와
|
|
|
49
49
|
3. Codex 에서는 이 명령이 네트워크와 `~/.codex` 쓰기를 하므로 `sandbox_permissions: "require_escalated"` 로 승인을 요청한다.
|
|
50
50
|
4. 출력의 `steps` 에 실패한 단계가 있으면 그 단계와 `why` 를 보여 주고 멈춘다. 설치되지 않은 것을 설치된 것처럼 말하지 않는다.
|
|
51
51
|
- 네트워크가 없으면 여기서 멈춘다. 오프라인에서는 이 스킬을 쓸 수 없다.
|
|
52
|
+
- `python` 단계가 "Python 3 을 찾지 못함" 으로 실패하면, 설치한 Python 이 PATH 밖에 있는지 사용자에게 묻는다. Windows 에서 `py` 가 없고 PATH 의 `python` 이 Microsoft Store 연결 파일뿐이거나, Miniconda 를 PATH 에 넣지 않고 설치한 경우다.
|
|
53
|
+
- 있으면 그 python 실행 파일의 절대 경로를 환경 변수 `MOTION_SITES_PYTHON` 에 영구로 지정하라고 안내한다. `%USERPROFILE%`, `~` 같은 줄임은 쓰지 않는다. 셸에 따라 글자 그대로 저장되어 엔진이 찾지 못한다.
|
|
54
|
+
- Windows(cmd 나 PowerShell): `setx MOTION_SITES_PYTHON "C:\Users\<이름>\miniconda3\python.exe"`
|
|
55
|
+
- macOS, Linux: 셸 설정 파일(`~/.zshrc`, `~/.bashrc`)에 `export MOTION_SITES_PYTHON=/절대/경로/python3`
|
|
56
|
+
- 한 번만 주는 것으로는 안 된다. 다음 실행의 상태 확인도 그 Python 을 봐야 한다. 이미 떠 있는 프로그램에는 새 값이 반영되지 않으므로, Claude Code 나 Codex 를 완전히 끝내고 다시 시작한 뒤 2단계를 다시 한다.
|
|
57
|
+
- Playwright 는 지정한 Python 에 설치된다. 그 Python 을 바꾸고 싶지 않으면 `<그 python> -m venv <폴더>` 로 가상 환경을 만들고 그 안의 python 을 지정한다(Windows `<폴더>\Scripts\python.exe`, macOS 와 Linux `<폴더>/bin/python`). pip 이 시스템 Python 을 거부하는(externally managed) 경우는 엔진이 캐시 폴더에 가상 환경을 스스로 만든다.
|
|
58
|
+
- 실패 이유가 "MOTION_SITES_PYTHON 에 지정한 ... 가 실행되지 않음" 이면 지정한 경로가 틀린 것이다. 경로를 고치게 한다.
|
|
59
|
+
- Python 이 없으면 Python 3 설치를 안내한다. 직접 설치하지 않는다.
|
|
52
60
|
- Linux 에서 Chromium 이 시스템 라이브러리 부족으로 뜨지 않으면 `<python.bin> -m playwright install-deps chromium` 을 안내한다. 관리자 권한이 필요하므로 직접 실행하지 않는다.
|
|
53
61
|
5. 끝나면 `ready` 가 `true` 인지 확인하고 3단계로 간다.
|
|
54
62
|
|
|
@@ -215,9 +215,16 @@ export function lookPython(py, runner = run) {
|
|
|
215
215
|
return { bin, cmd: py, playwright: pw, chromium: exe.ok && chrome !== "" && existsSync(chrome) };
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* The first Python with Playwright (and Chromium, when one has both), else the first that runs. A MOTION_SITES_PYTHON
|
|
220
|
+
* is the user's choice: only it and the cache's virtual environment (made from it when pip refuses it) are looked at,
|
|
221
|
+
* and when it does not run there is none, so Playwright never goes into a Python the user did not name.
|
|
222
|
+
*/
|
|
219
223
|
export function pickPython(env = process.env, runner = run) {
|
|
220
|
-
const
|
|
224
|
+
const all = pythons(env);
|
|
225
|
+
const named = env.MOTION_SITES_PYTHON ? lookPython(all[0], runner) : null;
|
|
226
|
+
if (env.MOTION_SITES_PYTHON && !named) return null;
|
|
227
|
+
const seen = (named ? [named, lookPython(all[1], runner)] : all.map((p) => lookPython(p, runner))).filter(Boolean);
|
|
221
228
|
return seen.find((p) => p.playwright && p.chromium) ?? seen.find((p) => p.playwright) ?? seen[0] ?? null;
|
|
222
229
|
}
|
|
223
230
|
|
|
@@ -265,10 +272,20 @@ function enablePlugin(runtime, runner) {
|
|
|
265
272
|
return { step: "plugin enable", ok: r.ok, why: r.why };
|
|
266
273
|
}
|
|
267
274
|
|
|
275
|
+
// No Python found: says where it looked and how to name one that is installed but not on PATH (Windows with only
|
|
276
|
+
// Miniconda, or only the Microsoft Store link, has no py and no working python).
|
|
277
|
+
export const NO_PYTHON = "Python 3 을 찾지 못함(python3, python, Windows 는 py). 설치한 Python 이 PATH 밖에 있으면 환경 변수 MOTION_SITES_PYTHON 에 그 python 실행 파일의 절대 경로를 영구로 지정한 뒤 다시 실행";
|
|
278
|
+
|
|
279
|
+
/** Why no Python was found; a MOTION_SITES_PYTHON that is set but does not run is named, so a typo is not hidden. */
|
|
280
|
+
export function noPython(env = process.env) {
|
|
281
|
+
if (!env.MOTION_SITES_PYTHON) return NO_PYTHON;
|
|
282
|
+
return `MOTION_SITES_PYTHON 에 지정한 ${pythons(env)[0][0]} 가 실행되지 않음. 경로를 고친 뒤 다시 실행. ${NO_PYTHON}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
268
285
|
/** The Python steps: Playwright, then Chromium. */
|
|
269
286
|
function installBrowser(env, runner) {
|
|
270
287
|
let py = pickPython(env, runner);
|
|
271
|
-
if (!py) return [{ step: "python", ok: false, why:
|
|
288
|
+
if (!py) return [{ step: "python", ok: false, why: noPython(env) }];
|
|
272
289
|
const steps = [];
|
|
273
290
|
let cmd = py.cmd;
|
|
274
291
|
if (!py.playwright) {
|
|
@@ -252,8 +252,10 @@ function gitFetch(args) {
|
|
|
252
252
|
// ── 1. 업데이트 ──────────────────────────────────────────────────────────────
|
|
253
253
|
function ensureVersion() {
|
|
254
254
|
const have = installedVersions();
|
|
255
|
-
if (have.includes(TARGET_VERSION)) return useCached();
|
|
256
|
-
|
|
255
|
+
if (have.includes(TARGET_VERSION) && completeVersion(TARGET_VERSION)) return useCached();
|
|
256
|
+
// 풀다 멈춘 폴더(hooks/hooks.json 이나 scripts/ 가 없음)는 설치된 것으로 보지 않는다. 그대로 전환하면 빈 플러그인으로 바뀐다.
|
|
257
|
+
if (have.includes(TARGET_VERSION)) bad(`${TARGET_VERSION} 캐시 폴더가 불완전함 (hooks/hooks.json 이나 scripts/ 가 없음)${MODE === "check" ? "" : ". 다시 풉니다"}`);
|
|
258
|
+
if (MODE === "check") { if (!have.includes(TARGET_VERSION)) bad(`${TARGET_VERSION} 미설치 (설치된 버전: ${have.join(", ") || "없음"})`); return false; }
|
|
257
259
|
if (!canInstallTarget()) return false;
|
|
258
260
|
|
|
259
261
|
log(` ${TARGET_VERSION} 설치 중...`);
|
|
@@ -265,6 +267,9 @@ function ensureVersion() {
|
|
|
265
267
|
return switchActive(sha);
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
// 판 폴더에 훅 목록과 스크립트 폴더가 있는가.
|
|
271
|
+
const completeVersion = (v) => existsSync(join(CACHE, v, "hooks", "hooks.json")) && existsSync(join(CACHE, v, "scripts"));
|
|
272
|
+
|
|
268
273
|
// TARGET_VERSION 이 캐시에 있을 때. 이미 활성이면 끝, 아니면 마켓플레이스 태그의 커밋으로 전환한다.
|
|
269
274
|
// 전환도 마켓플레이스와 git 이 있어야 하므로 먼저 확인한다(풀지 않으므로 tar 는 없어도 된다).
|
|
270
275
|
function useCached() {
|
|
@@ -356,16 +361,27 @@ function targetSha() {
|
|
|
356
361
|
}
|
|
357
362
|
|
|
358
363
|
// git archive | tar 로 소스를 편다. 저장소에 dist/ 가 포함되어 빌드는 불필요하다.
|
|
364
|
+
// 옆의 난수 이름 폴더에 다 푼 뒤에만 판 이름으로 옮긴다. 도중에 멈추면(tar 실패, 신호, 디스크 부족) 판 이름의 폴더는 생기지 않아,
|
|
365
|
+
// 다음 실행이 반쯤 찬 폴더를 설치된 판으로 보고 전환하지 않는다. 옆 폴더는 판 이름 꼴이 아니라 installedVersions 가 세지 않는다.
|
|
359
366
|
function unpackTarget(dest) {
|
|
360
|
-
|
|
361
|
-
mkdirSync(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
367
|
+
const part = `${dest}.part-${randomBytes(6).toString("hex")}`;
|
|
368
|
+
mkdirSync(part, { recursive: true });
|
|
369
|
+
try {
|
|
370
|
+
git(["archive", `v${TARGET_VERSION}`, "-o", join(part, "_src.tar")]);
|
|
371
|
+
execFileSync("tar", ["-xf", join(part, "_src.tar"), "-C", part], { cwd: homedir(), stdio: "ignore" });
|
|
372
|
+
unlinkSync(join(part, "_src.tar"));
|
|
373
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true });
|
|
374
|
+
renameSync(part, dest);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
rmSync(part, { recursive: true, force: true });
|
|
377
|
+
throw e;
|
|
378
|
+
}
|
|
365
379
|
}
|
|
366
380
|
|
|
367
381
|
// node_modules 는 복사하지 않고 직전 버전 것을 링크한다. Windows 는 junction
|
|
368
382
|
// (관리자 권한 불필요), POSIX 는 type 인자가 무시되고 디렉터리 symlink 가 된다.
|
|
383
|
+
// 없어도 전환을 막지 않는다. Claude Code 가 직접 설치한 OMC 판에는 node_modules 가 없고, 실행에 쓰는 것은 dist/ 에
|
|
384
|
+
// 묶여 있다(2026-11-03 실측: 5.7.21~5.7.30 모두 없음, OMC 정상 동작). 막으면 보통 PC 에서 전환이 늘 실패한다.
|
|
369
385
|
function linkNodeModules(dest, have) {
|
|
370
386
|
const prev = have.find((v) => v !== TARGET_VERSION);
|
|
371
387
|
const nm = join(dest, "node_modules");
|
|
@@ -699,16 +715,22 @@ function openParenOf(src, j) {
|
|
|
699
715
|
// 그 상태를 «패치됨» 으로 넘기면 --check 가 결함 트리에 «모두 적용» 을 낸다(2026-10-12 실측).
|
|
700
716
|
function siteInfo(src) {
|
|
701
717
|
if (src.includes("__omcRaceImport")) {
|
|
702
|
-
|
|
718
|
+
// 헬퍼가 있어도 적용이 감쌌어야 할 자리가 남았으면(패치 뒤 상류가 줄을 더함) 적용된 것이 아니다.
|
|
719
|
+
// 헬퍼 본문의 Promise.race 는 손으로 두른 것이 아니므로, 같은 길이의 공백으로 지운 글에서 찾는다(자리는 그대로).
|
|
720
|
+
const left = openSites(src.split(HELPER).join(" ".repeat(HELPER.length)));
|
|
721
|
+
const why = helperDefect(scanFacts(src)) ?? (left.length ? `감싸지 않은 최상위 await import 가 남음 :${left.map((p) => src.slice(0, p).split("\n").length).join(", :")}` : null);
|
|
703
722
|
return why ? { state: "misplaced", sites: [], why } : { state: "patched", sites: [] };
|
|
704
723
|
}
|
|
705
724
|
const all = scanTopLevelAwaitImports(src);
|
|
706
725
|
if (!all.length) return { state: "n/a", sites: [] };
|
|
707
|
-
const open =
|
|
726
|
+
const open = openSites(src);
|
|
708
727
|
if (!open.length) return { state: "guarded", sites: [] };
|
|
709
728
|
return { state: "needs", sites: open };
|
|
710
729
|
}
|
|
711
730
|
|
|
731
|
+
// 적용이 감싸는 자리: 최상위 await import 가운데 바로 앞(300자)에 손으로 두른 Promise.race 가 없는 것.
|
|
732
|
+
const openSites = (src) => scanTopLevelAwaitImports(src).filter((p) => !/Promise\.race/.test(src.slice(Math.max(0, p - 300), p)));
|
|
733
|
+
|
|
712
734
|
// 헬퍼가 쓰일 수 없는 자리면 그 이유를, 제자리면 null 을 낸다.
|
|
713
735
|
// - 선언이 블록 안(깊이 1 이상): 다른 블록의 자리에서 ReferenceError(옛 킷이 남긴 자리)
|
|
714
736
|
// - 선언을 못 찾음: 이름만으로는 «패치됨» 이라 할 근거가 없다. 앞쪽 글을 토크나이저가 잘못
|
|
@@ -834,15 +856,33 @@ function patchHooks(root) {
|
|
|
834
856
|
// 4.15.7 이 둘 다 겪었다 — PostToolUse 를 지우면 그 스크립트가 목록에서 빠지고(그래서
|
|
835
857
|
// .omcbak 이 있어도 안 돌아온다), 손으로 고친 파일은 .omcbak 조차 없어 아예 안 보인다.
|
|
836
858
|
const names = MODE === "revert" ? scriptNames(root, r) : hookNames(root);
|
|
859
|
+
// 고칠 훅을 하나도 찾지 못했으면 «실패 0건» 이 아니다. 목록이 빈 hooks.json(`{}`, `[]`)은 고친 것도 확인한 것도 없다.
|
|
860
|
+
// hooks.json 이 없거나 읽히지 않으면 [3/4] 가 이름과 이유를 대고 센다. 여기서는 읽혔는데 훅이 없을 때만 센다.
|
|
861
|
+
if (MODE !== "revert" && !names.length) {
|
|
862
|
+
if (readableFile(join(root, "hooks", "hooks.json"))) notChecked(r, join(root, "hooks", "hooks.json"), "hooks.json 이 부르는 훅(scripts/*.mjs)을 하나도 찾지 못함");
|
|
863
|
+
return r;
|
|
864
|
+
}
|
|
837
865
|
for (const n of names) {
|
|
838
866
|
const f = join(root, "scripts", `${n}.mjs`);
|
|
839
|
-
if (
|
|
840
|
-
|
|
841
|
-
|
|
867
|
+
if (MODE === "revert") { if (existsSync(f)) revertScript(root, n, f, r); continue; }
|
|
868
|
+
// hooks.json 이 부르는데 파일이 없으면 이름을 대고 센다. 그 밖에 볼 수 없으면(권한) readHook 이 이유를 대고 센다.
|
|
869
|
+
if (missingFile(f)) { notChecked(r, f, `hooks.json 이 부르는 훅 파일이 없음: ${shown(n)}`); continue; }
|
|
870
|
+
checkOrPatchScript(root, n, f, r);
|
|
842
871
|
}
|
|
843
872
|
return r;
|
|
844
873
|
}
|
|
845
874
|
|
|
875
|
+
// 확인하지 못한 것: 이름과 이유를 대고 실패로 센다. check 는 «모두 적용» 이라 하지 않게 읽지 못한 것으로도 센다.
|
|
876
|
+
function notChecked(r, f, why) {
|
|
877
|
+
bad(why);
|
|
878
|
+
r.fail++;
|
|
879
|
+
r.failed.push(f);
|
|
880
|
+
if (MODE === "check") r.unread++;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const readableFile = (f) => { try { readRegular(f); return true; } catch { return false; } };
|
|
884
|
+
const missingFile = (f) => { try { lstatSync(f); return false; } catch (e) { return e.code === "ENOENT"; } };
|
|
885
|
+
|
|
846
886
|
// revert 가 훑는 이름: scripts/ 의 .mjs 전부. scripts/ 가 없는 판 폴더는 되돌릴 훅이 없어 한 줄로 알리고 넘어간다.
|
|
847
887
|
// 그 밖에 목록을 읽지 못하면(파일, 권한) 이름과 이유를 대고 실패로 센다 — 다음 판과 [3/4], [4/4] 는 그대로 간다.
|
|
848
888
|
function scriptNames(root, r) {
|
|
@@ -927,12 +967,20 @@ function handRestoreHint(bak, f) {
|
|
|
927
967
|
|
|
928
968
|
// 백업의 내용으로 파일을 바꾼다. 쓰지 못하면 그 이유, 백업이 읽으려는 순간 사라졌으면 GONE, 아니면 null.
|
|
929
969
|
const GONE = "백업이 먼저 사라짐";
|
|
970
|
+
// 빈 백업이나 문법 검사를 통과하지 못하는 백업(잘린 복사)으로는 되돌리지 않는다. 쓴 뒤 검사에 걸리면 쓰기 전 글로 되돌린다.
|
|
930
971
|
function restoreFrom(f, bak) {
|
|
931
972
|
let data;
|
|
932
973
|
try { data = readRegular(bak); }
|
|
933
974
|
catch (e) { return e.code === "ENOENT" ? GONE : `읽지 못함: ${failWhy(e)}`; }
|
|
934
|
-
|
|
975
|
+
if (!data.length) return "백업이 비어 있음";
|
|
976
|
+
let before;
|
|
977
|
+
try { before = readRegular(f); writeAtomic(f, data); }
|
|
935
978
|
catch (e) { return `쓰지 못함: ${failWhy(e)}`; }
|
|
979
|
+
const err = syntaxError(f);
|
|
980
|
+
if (!err) return null;
|
|
981
|
+
try { writeAtomic(f, before); }
|
|
982
|
+
catch (e) { return `백업이 문법 검사를 통과하지 못했고 원래 글로도 되돌리지 못함 (${failWhy(e)}): ${oneLine(err)}`; }
|
|
983
|
+
return `백업이 문법 검사를 통과하지 못함: ${oneLine(err)}`;
|
|
936
984
|
}
|
|
937
985
|
|
|
938
986
|
// 되돌린 뒤 백업을 지운다. 다른 실행이 먼저 지웠으면(ENOENT) 된 것이다. 못 지우면 그 이유, 아니면 null.
|
|
@@ -948,6 +996,9 @@ function checkOrPatchScript(root, n, f, r) {
|
|
|
948
996
|
// 헬퍼 자리 결함은 check 와 apply 모두 이름을 댄다. 이미 감싼 파일이라 transform 으로는
|
|
949
997
|
// 고칠 수 없으므로 apply 도 건드리지 않고, 되돌린 뒤 다시 적용하라고 안내한다(misplacedHint).
|
|
950
998
|
if (info.state === "misplaced") { bad(`헬퍼 자리 결함: ${n} (${info.why})`); r.misplaced++; return; }
|
|
999
|
+
// 헬퍼 이름이 있어도 잘린 파일(복사가 끊김)은 돌지 않는다. 문법 검사를 통과해야 패치된 것으로 센다.
|
|
1000
|
+
const broken = info.state === "patched" ? syntaxError(f) : null;
|
|
1001
|
+
if (broken) { bad(`헬퍼 자리 결함: ${n} (패치된 파일이 문법 검사를 통과하지 못함: ${oneLine(broken)})`); r.misplaced++; return; }
|
|
951
1002
|
if (info.state !== "needs") { r.skip++; return; }
|
|
952
1003
|
const bak = `${f}.omcbak`;
|
|
953
1004
|
const why = whyNotWrite(root, f, bak);
|
|
@@ -1079,6 +1130,7 @@ function revertHooksJson(root, hj) {
|
|
|
1079
1130
|
const bak = `${hj}.omcbak`;
|
|
1080
1131
|
const saved = readBackup(bak);
|
|
1081
1132
|
if (saved !== null) {
|
|
1133
|
+
if (!isJson(saved)) { hjBad("hooks.json 을 되돌리지 않음 (백업이 비었거나 JSON 이 아님)"); return "unsafe"; }
|
|
1082
1134
|
if (staleHooksJson(hj, saved)) { hjBad(`hooks.json 을 되돌리지 않음 (지금 파일에 PostToolUse 가 있고 백업과 글이 다름. 백업이 옛 원본일 수 있음)`); return "unsafe"; }
|
|
1083
1135
|
writeAtomic(hj, saved);
|
|
1084
1136
|
log(` 되돌림 ${hjTag}hooks.json`);
|
|
@@ -1102,9 +1154,14 @@ function revertHooksJson(root, hj) {
|
|
|
1102
1154
|
return "reverted";
|
|
1103
1155
|
}
|
|
1104
1156
|
if (usable.length > 1) { hjBad(`hooks.json 수동 백업이 여러 개라 자동 복구하지 않음: ${usable.map(shown).join(", ")}`); return "ambiguous"; }
|
|
1105
|
-
|
|
1157
|
+
if (unsafe.length) return "linked";
|
|
1158
|
+
// 깨진 hooks.json 에 되돌릴 백업도 없으면 «이 도구가 지운 적 없음» 이라 할 근거가 없다. 그대로 두고 실패로 센다.
|
|
1159
|
+
if (!isJson(readHooksJson(hj))) { hjBad("hooks.json 이 JSON 이 아니고 되돌릴 백업도 없음"); return "unsafe"; }
|
|
1160
|
+
return "no-backup";
|
|
1106
1161
|
}
|
|
1107
1162
|
|
|
1163
|
+
const isJson = (text) => { try { JSON.parse(String(text)); return true; } catch { return false; } };
|
|
1164
|
+
|
|
1108
1165
|
// 파일에서 읽은 값(파일 이름, PINNED 의 host)과 출력하는 경로를 출력에 넣을 때 쓴다. 줄바꿈과 제어 문자를 ? 로 바꾸고 길이를 자른다.
|
|
1109
1166
|
// SKILL 은 [!] 줄을 그대로 보고하라고 하므로, 값이 제 줄을 새로 시작하면 도구가 낸 줄처럼 읽힌다.
|
|
1110
1167
|
// 바꾸는 문자: 제어 문자(Cc), 서식 문자(Cf: 폭 없는 공백, 방향 표시, 단어 결합자, 태그 문자), 줄과 문단 구분자,
|
|
@@ -1270,7 +1327,7 @@ function removePostToolUse(hj, why) {
|
|
|
1270
1327
|
// 고정과 되돌리기가 하지 않은 일을 했다고 끝난다.
|
|
1271
1328
|
function originUrl() {
|
|
1272
1329
|
try { return git(["config", "--get", "remote.origin.url"]); }
|
|
1273
|
-
catch (e) { return e.status === 1 ? null : { unreadable: gitLine(e) }; }
|
|
1330
|
+
catch (e) { return e.status === 1 && !String(e.stderr || "").trim() ? null : { unreadable: gitLine(e) }; }
|
|
1274
1331
|
}
|
|
1275
1332
|
|
|
1276
1333
|
// git 오류의 원인 줄: `fatal:` 이나 `error:` 로 시작하는 첫 줄, 없으면 첫 줄(`warning:` 이 먼저 올 수 있다), 정화하고
|
|
@@ -1688,7 +1745,8 @@ for (const v of targets) {
|
|
|
1688
1745
|
else if (hj === "needs") hjNeedBy[v] = true;
|
|
1689
1746
|
else if (hj === "reverted" || hj === "ambiguous" || hj === "linked") { /* patchHooksJson 이 출력함 */ }
|
|
1690
1747
|
else if (hj === "no-backup") ok(`${tag}되돌릴 백업 없음 (이 도구가 지운 적 없음)`);
|
|
1691
|
-
|
|
1748
|
+
// 판 폴더에 hooks.json 이 없으면 고친 것도 확인한 것도 없다. 실패로 센다.
|
|
1749
|
+
else if (hj === "no-file") { bad(`${tag}hooks.json 파일 자체가 없음`); r.fail++; hjFailBy[v] = true; failed.add(fileKey(join(root, "hooks", "hooks.json"))); }
|
|
1692
1750
|
// "failed" 는 고친 hooks.json 이 JSON 으로 읽히지 않아 되돌린 경우다. 고치지 못했으니 실패로 센다.
|
|
1693
1751
|
else if (hj === "unsafe" || hj === "failed") { r.fail++; hjFailBy[v] = true; failed.add(fileKey(join(root, "hooks", "hooks.json"))); }
|
|
1694
1752
|
if (hj === "ambiguous" || hj === "linked") left.add(fileKey(join(root, "hooks", "hooks.json")));
|
package/skills/ralph-qa/SKILL.md
CHANGED
|
@@ -408,6 +408,7 @@ node '<R>/gate-evidence.mjs' cleanup --dir '<g>'
|
|
|
408
408
|
|
|
409
409
|
### 3. 판정 수신
|
|
410
410
|
각 좌석에서 4값 중 하나를 받는다. 외부 좌석은 **단독 줄 `VERDICT: <값>`** 이 필수이며, 부재·중복·절단·파싱실패·거부는 `ERROR` 다.
|
|
411
|
+
코드펜스(```, ~~~) 안의 `VERDICT:` 줄은 인용한 글이라 세지 않는다. 판정 줄을 코드펜스로 감싼 답은 판정 줄이 없는 답이라 `ERROR` 다. send 출력의 `answered`, `failed` 는 답한 좌석과 실패한 좌석의 수다. `ok: true` 는 send 가 돌았다는 뜻일 뿐이고, 실패한 좌석의 `facts` 에는 `verdict` 가 없다.
|
|
411
412
|
|
|
412
413
|
**나눠 보낸 페이로드.** 한 좌석에 나눠 보낸 부분들은 그 좌석의 한 판정이다. 모든 부분이 `APPROVE` 일 때만 `APPROVE`, 하나라도 `ERROR` 면 `ERROR`, 그 밖은 가장 나쁜 값(`REJECT` 가 `ITERATE` 보다 나쁘다)이다. 부분끼리는 서로의 내용을 모르므로 좌석 줄에 `분할 N` 을 적는다.
|
|
413
414
|
|
|
@@ -601,11 +601,24 @@ const readText = (file) => {
|
|
|
601
601
|
}
|
|
602
602
|
};
|
|
603
603
|
|
|
604
|
+
// The standalone verdict lines of an answer. A line inside a code fence (``` or ~~~) is quoted text, not the seat's verdict.
|
|
605
|
+
export function verdictLines(out) {
|
|
606
|
+
let fence = null;
|
|
607
|
+
const found = [];
|
|
608
|
+
for (const line of out.split(/\r?\n/)) {
|
|
609
|
+
const mark = /^\s{0,3}(`{3,}|~{3,})/.exec(line);
|
|
610
|
+
if (mark && (!fence || (mark[1][0] === fence[0] && mark[1].length >= fence.length))) { fence = fence ? null : mark[1]; continue; }
|
|
611
|
+
const m = !fence && VERDICT_LINE.exec(line);
|
|
612
|
+
if (m) found.push(m[1]);
|
|
613
|
+
}
|
|
614
|
+
return found;
|
|
615
|
+
}
|
|
616
|
+
|
|
604
617
|
// What a seat's answer and errors say.
|
|
605
618
|
function factsOf(cli, plan) {
|
|
606
619
|
const out = readText(plan.answer);
|
|
607
620
|
const err = readText(plan.stderr);
|
|
608
|
-
const facts = { verdicts: out
|
|
621
|
+
const facts = { verdicts: verdictLines(out), empty: !out.trim() };
|
|
609
622
|
if (cli === "codex") facts.ranModel = (/^model: (\S+)/m.exec(err.split("\n").slice(0, 40).join("\n")) || [])[1] || null;
|
|
610
623
|
if (cli === "gemini") Object.assign(facts, { policyError: err.includes("Policy file error"), apiError: err.includes("Full report") });
|
|
611
624
|
if (cli === "opencode") facts.fallback = err.includes("Falling back to default agent");
|
|
@@ -665,10 +678,12 @@ async function runParts(ctx, run, seat, g, parts, base) {
|
|
|
665
678
|
}
|
|
666
679
|
const last = done[done.length - 1];
|
|
667
680
|
const secs = Math.round(done.reduce((t, x) => t + (x.r.secs || 0), 0) * 10) / 10;
|
|
681
|
+
// a failed seat has no verdict to count, so its facts carry none (a reader that skips `status` would count it)
|
|
682
|
+
const shownFacts = (x) => { const f = factsShown(x.f); if (x.why) delete f.verdict; return f; };
|
|
668
683
|
const result = { ...base, status: last.why ? "failed" : "answered", why: last.why, exit: last.r.exit, signal: last.r.signal, secs,
|
|
669
|
-
answer: last.plan.answer, errors: last.plan.stderr, facts:
|
|
684
|
+
answer: last.plan.answer, errors: last.plan.stderr, facts: shownFacts(last) };
|
|
670
685
|
if (!last.why && parts.length > 1) result.facts.verdict = gathered(done.map((x) => x.f.verdicts[0]));
|
|
671
|
-
if (parts.length > 1) result.parts = done.map((x) => ({ answer: x.plan.answer, errors: x.plan.stderr, why: x.why, facts:
|
|
686
|
+
if (parts.length > 1) result.parts = done.map((x) => ({ answer: x.plan.answer, errors: x.plan.stderr, why: x.why, facts: shownFacts(x) }));
|
|
672
687
|
return result;
|
|
673
688
|
}
|
|
674
689
|
|
|
@@ -838,7 +853,9 @@ async function send(o, ctx) {
|
|
|
838
853
|
if (thrown) throw thrown.reason;
|
|
839
854
|
const seats = settled.map((x) => x.value);
|
|
840
855
|
if (!a.dryRun) appendFileSync(join(d, "run.md"), seats.filter((x) => x.cliVersion).map((x) => `cli-${x.seat} ${x.cliVersion}\n`).join(""), { mode: 0o600 });
|
|
841
|
-
|
|
856
|
+
// ok says the send ran; whether seats answered is in `answered`, `failed` and each seat's status
|
|
857
|
+
const answered = seats.filter((x) => x.status === "answered").length;
|
|
858
|
+
const result = { ok: true, ...(a.dryRun ? { dryRun: true } : {}), dir: d, out, bytes: payload.length, scan: 0, unsent: unsentCount(d), answered, failed: seats.filter((x) => x.status === "failed").length, seats, declared: declared(run, payload.length, seats, d) };
|
|
842
859
|
if (!a.dryRun) writeResult(d, "send", resultJson("send", result));
|
|
843
860
|
return [result, signals.code(), a.dryRun ? null : d];
|
|
844
861
|
} finally {
|