cuelo 0.10.91 → 0.10.92

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.
Files changed (75) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +27 -27
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/server/app/api/updates/route.js +1 -1
  26. package/.next/server/app/index.html +1 -1
  27. package/.next/server/app/index.rsc +4 -4
  28. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  29. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  30. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  31. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  32. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  33. package/.next/server/app/page.js +2 -2
  34. package/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/recover.html +1 -1
  37. package/.next/server/app/recover.rsc +2 -2
  38. package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
  39. package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
  41. package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
  43. package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
  44. package/.next/server/app-paths-manifest.json +27 -27
  45. package/.next/server/middleware-build-manifest.js +1 -1
  46. package/.next/server/pages/404.html +1 -1
  47. package/.next/server/pages/500.html +1 -1
  48. package/.next/server/server-reference-manifest.json +1 -1
  49. package/.next/static/chunks/app/{layout-a4659d1d9b2f7cd2.js → layout-1b0f7d55bb614d51.js} +1 -1
  50. package/.next/static/chunks/app/{page-28a6977a004bea8f.js → page-496491963af99497.js} +2 -2
  51. package/.next/trace +108 -108
  52. package/.next/trace-build +1 -1
  53. package/README.md +2 -0
  54. package/Tools/CUELO_Setup/files/native-runtime-patch.js +1 -1
  55. package/Tools/OMP_Global_Config/agent/RULES.md +16 -12
  56. package/Tools/OMP_Global_Config/agent/agents/maker.md +5 -3
  57. package/Tools/OMP_Global_Config/agent/extensions/computer-task.ts +45 -0
  58. package/Tools/OMP_Global_Config/agent/extensions/cross-review.ts +12 -6
  59. package/Tools/OMP_Global_Config/agent/extensions/draft-coverage.ts +146 -0
  60. package/Tools/OMP_Global_Config/agent/extensions/lib/computer-task.ts +289 -0
  61. package/Tools/OMP_Global_Config/agent/extensions/lib/cross-review.ts +500 -93
  62. package/Tools/OMP_Global_Config/agent/extensions/lib/draft-coverage.ts +130 -0
  63. package/Tools/OMP_Global_Config/agent/extensions/lib/routing-ledger.ts +19 -4
  64. package/Tools/OMP_Global_Config/agent/extensions/lib/slop-gate.ts +35 -129
  65. package/Tools/OMP_Global_Config/agent/extensions/slop-gate/review.ts +9 -16
  66. package/Tools/OMP_Global_Config/agent/extensions/slop-gate.ts +13 -21
  67. package/Tools/OMP_Global_Config/agent/prompt-copies/RULES.en.md +5 -4
  68. package/Tools/OMP_Global_Config/agent/rules/harness-policy.json +6 -6
  69. package/Tools/OMP_Global_Config/agent/rules/meta-harness.md +1 -1
  70. package/Tools/OMP_Global_Config/agent/rules/subagent.md +34 -10
  71. package/Tools/OMP_Global_Config/agent/sop/maker.md +4 -2
  72. package/Tools/OMP_Global_Config/agent/tools/git-finalizer/index.ts +3 -6
  73. package/package.json +1 -1
  74. /package/.next/static/{7PldK6Sn5lDzk3X-aih8W → 64pA_X2VV_EHqVo9AxR5F}/_buildManifest.js +0 -0
  75. /package/.next/static/{7PldK6Sn5lDzk3X-aih8W → 64pA_X2VV_EHqVo9AxR5F}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-webpack","duration":82973662,"timestamp":489988,"id":14,"parentId":1,"tags":{},"startTime":1793058095646,"traceId":"1d4ee7125dab70c2"},{"name":"run-typescript","duration":18056255,"timestamp":83832852,"id":11043,"parentId":1,"tags":{},"startTime":1793058178989,"traceId":"1d4ee7125dab70c2"},{"name":"static-check","duration":3312783,"timestamp":101967808,"id":11046,"parentId":1,"tags":{},"startTime":1793058197124,"traceId":"1d4ee7125dab70c2"},{"name":"static-generation","duration":6890335,"timestamp":105371516,"id":11218,"parentId":1,"tags":{},"startTime":1793058200528,"traceId":"1d4ee7125dab70c2"},{"name":"collect-build-traces","duration":18753302,"timestamp":105348878,"id":11215,"parentId":1,"tags":{},"startTime":1793058200505,"traceId":"1d4ee7125dab70c2"},{"name":"telemetry-flush","duration":89,"timestamp":124106296,"id":11227,"parentId":1,"tags":{},"startTime":1793058219262,"traceId":"1d4ee7125dab70c2"},{"name":"next-build","duration":123695212,"timestamp":411200,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1793058095567,"traceId":"1d4ee7125dab70c2"}]
1
+ [{"name":"run-webpack","duration":60053199,"timestamp":335898,"id":14,"parentId":1,"tags":{},"startTime":1793082430573,"traceId":"379e70c785a51aa8"},{"name":"run-typescript","duration":11704160,"timestamp":60716904,"id":11043,"parentId":1,"tags":{},"startTime":1793082490954,"traceId":"379e70c785a51aa8"},{"name":"static-check","duration":2268895,"timestamp":72502139,"id":11046,"parentId":1,"tags":{},"startTime":1793082502739,"traceId":"379e70c785a51aa8"},{"name":"static-generation","duration":4271006,"timestamp":74862828,"id":11218,"parentId":1,"tags":{},"startTime":1793082505100,"traceId":"379e70c785a51aa8"},{"name":"collect-build-traces","duration":12969690,"timestamp":74848506,"id":11215,"parentId":1,"tags":{},"startTime":1793082505086,"traceId":"379e70c785a51aa8"},{"name":"telemetry-flush","duration":55,"timestamp":87824978,"id":11227,"parentId":1,"tags":{},"startTime":1793082518062,"traceId":"379e70c785a51aa8"},{"name":"next-build","duration":87549243,"timestamp":275806,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1793082430513,"traceId":"379e70c785a51aa8"}]
package/README.md CHANGED
@@ -165,6 +165,8 @@ Requires Node.js 22.19+ and Bun 1.4.2+. Open http://127.0.0.1:30141 and sign in
165
165
 
166
166
  문서 하네스는 기존 Word·Excel·PowerPoint 문서를 열고 편집하거나 변환할 때 LibreOffice를 우선 사용합니다. 원본은 보존하고 출력 결과를 확인하며, 매크로·전용 기능·서식 호환성이 필요하거나 사용자가 지정하면 MS Office를 사용합니다.
167
167
 
168
+ 문서를 작성하거나 편집할 때 초안에 없는 문구가 상당 부분 추가되면, 반영하기 전에 해당 문구를 짚어 초안 작성을 안내합니다. 적용 범위와 예외는 [문서 초안 작성 안내](./docs/harness.md#문서-초안-작성-draft)를 참고하세요.
169
+
168
170
  장기 백그라운드 작업은 관측 기한이 되면 대기를 깨워 상태를 확인하게 합니다. 이미 끝난 결과는 먼저 회수하며, 작업을 자동으로 취소하거나 재실행하지 않습니다. [하네스 안내](./docs/harness.md#발주와-검수-계약)에 적용 범위와 확인 방법이 있습니다.
169
171
 
170
172
  취소되거나 시간초과된 하위 작업의 늦은 완료 내용은 기존 수용 결과를 덮어쓰지 않습니다. 중단 상태는 성공으로 바뀌지 않으며, 진단용 내용과 기록된 사용량은 별도로 남습니다. 이미 실행된 외부 작업이나 과금을 되돌리는 기능은 아닙니다.
@@ -9,7 +9,7 @@ const MEMORY_RE = /(const phase1Model = await resolveMemoryModel\(\{[^}]*?fallba
9
9
  const MEMORY_DONE = /const phase1Model = await resolveMemoryModel\(\{[^}]*?fallbackRole: "smol"/;
10
10
  const EXPECTED = {
11
11
  name: 'cuelo',
12
- version: '0.10.91',
12
+ version: '0.10.92',
13
13
  distribution: 'cuelo',
14
14
  coreVersion: '18.13.0',
15
15
  };
@@ -92,20 +92,21 @@
92
92
  `edit`로 반영한다. 코드·설정·규칙 파일(`RULES.md`·`AGENTS.md`·`rules/`·SOP), 몇 줄 이하의 정확한 수정, 사용자에게
93
93
  하는 대화 답변은 대상이 아니다. 이 Gemini는 2026-10-26부터 Google Cloud Vertex의 Gemini 4 Argon이며(크레딧으로
94
94
  과금) OCR 역할에도 쓴다. 이미지 질문(vision)은 Opus를 유지하고, Antigravity OAuth는 계정 정지 위험으로 쓰지 않는다.
95
- **문서의 AI 슬롭·교차검수는 공개 배포·공유용 산출물에만 적용한다(2026-10-26 사용자 정정).** 비공개·내부 문서는
96
- 파일로 사용자에게 전달하더라도 제외한다. 소스 코드 밖에 있거나 md·HTML·PPT·Word·Excel·PDF·HWPX·csv·UI 문구
97
- 형식이라는 이유만으로 공개 산출물로 보지 않는다. 공개 미러의 `.publicignore`(부정 규칙 포함), 프로젝트의
98
- `.slop-gate.json` `visible` 선언, 명시적인 공개용 지정으로 판별한다. 공개 여부가 불명확하면 자동으로 검토 모델에
99
- 보내거나 게이트로 막지 않는다. 비공개·내부 제외는 명시적 공개 지정으로 우회하지 않는다. 코드·주석·로그·테스트·생성물,
100
- 내부 프롬프트·규칙·스킬과 `HANDOFF.md`·`doc/history`·`doc/README.md`도 제외한다. 위 초안 작성·직접 교정 규칙과
101
- 코드 변경의 `cross_review`는 그대로 유지한다.
102
- 공개 산출물 점검은 `slop_check`로 하고 검토 모델은 필수 반대 계열 + Argon 보조 자문 경로를 쓴다. 지적이 없으면
95
+ **소스 코드 안에 내장된 내부 말고 바깥에 보여지는 산출물은 모두 AI 슬롭 게이트를 통과한다(2026-10-26 사용자 지시,
96
+ 2026-10-27 재확인).** 대상은 형식으로 정한다: README·CHANGELOG·`docs/`와 그 밖의 md, HTML, PPTX·DOCX·XLSX·PDF·HWPX,
97
+ 보고서 csv, 언어 팩의 화면 문구다. 공개 저장소에 올라가는지, `.publicignore`·`.slop-gate.json` 선언이 있는지는 따지지
98
+ 않는다. 고객·사용자에게 파일로 건네는 덱·보고서·문서도 대상이다(2026-10-27: 공개 선언이 없다는 이유로 고객용 덱이 빠진
99
+ 사례를 사용자가 바로잡았다). 제외는 소스 내부뿐이다: 코드·주석·로그·테스트·fixture·생성물·vendor, 내부 프롬프트·규칙·
100
+ SOP·스킬, `HANDOFF.md`·`doc/history`·`doc/README.md`. `.slop-gate.json` `visible`은 대상을 더하기만 한다. 위 초안 작성·
101
+ 직접 교정 규칙과 코드 변경의 `cross_review`는 그대로 유지한다.
102
+ 점검은 `slop_check`로 하고 검토 모델은 필수 반대 계열 + Argon 보조 자문 경로를 쓴다. 지적이 없으면
103
103
  그 파일 내용에 통과가 기록된다. 고친 파일은 다시 점검하며 내용을 바꾸지 않고 닫을 수 있는 것은 Main이 근거로
104
- 반박(`refuted`)한 지적뿐이다. git 추적 파일은 HEAD 대비 바뀐 글만 본다. 공개 대상만 `git_finalize`의 `slop-gate`와
105
- 저장소 밖 산출물의 턴 종료 검사에 걸린다. 공개 대상의 게이트 면제는 사용자의 명시 지시가 있을 때만 기록해 적용한다.
104
+ 반박(`refuted`)한 지적뿐이다. git 추적 파일은 HEAD 대비 바뀐 글만 본다. 대상 파일은 `git_finalize`의 `slop-gate`와
105
+ 저장소 밖 산출물의 턴 종료 검사에 걸린다. 게이트 면제는 사용자의 명시 지시가 있을 때만 기록해 적용한다.
106
106
  검토 응답이 JSON으로 해석되지 않으면 실패 원문·파싱 오류를 먼저 확인한다. 토큰 길이로 원인을 추측하거나 같은 조건으로
107
107
  반복 호출하지 않는다. 검토 응답의 형식 오류를 피하려고 산출물의 따옴표·쉼표·내용을 바꾸지 않는다. 실제 내용상 지적에
108
108
  근거한 교정은 별개다. 불완전한 JSON을 임의로 보완하거나 필수 검토가 실패한 상태에서 통과시키지 않는다.
109
+ - **draft는 호출 횟수가 아니라 작성 범위로 적용한다(2026-10-27 사용자 지적).** 일부 문단의 초안만 받은 뒤 나머지 설명·표 문구·슬라이드 본문을 Main이나 Maker가 직접 써서 합치면 안 된다. 새로 쓰거나 문단 단위로 다시 쓰는 각 블록은 최종 파일에 반영하기 전에 `draft`로 작성한다. 작업을 여러 번 나누거나 HTML 한 줄에 넣어도 예외가 되지 않는다. 초안 밖의 새 설명·예시·결론을 덧붙이는 것은 사실 교정이나 슬롭 제거가 아니다. 기존 문구의 정확한 사실·오탈자 교정, 문구를 바꾸지 않는 코드·레이아웃 변경은 직접 수정할 수 있다. 적용 전에 초안과 반영할 문구를 대조하고 빠진 블록만 다시 작성한다. `slop_check` 통과는 draft 작성 범위를 증명하지 않는다.
109
110
  - **문서 기본 편집 도구는 LibreOffice다(2026-10-15 사용자 결정).** 기존 Word·Excel·PowerPoint 문서의
110
111
  열기·편집·변환은 Writer·Calc·Impress를 우선 사용한다. 새 문서 초안은 위 `draft` 규칙을 따른다.
111
112
  MS Office는 사용자가 지정했거나, 매크로·외부 연결·전용 기능·서식 호환성 때문에 필요한 경우에 쓴다.
@@ -132,7 +133,10 @@
132
133
  가를 수 없는 판정("오류 대화상자가 떴나", "수용 조건과 맞나")만 같은 셀에서 `judge()`(JEV)에 텍스트로
133
134
  넘기고, 모양을 봐야 할 때만 스크린샷을 찍는다. `judge()` 결과는 참고 신호이며 최종 수용 근거가 아니다.
134
135
  `computer`는 사용자의 실제 데스크톱을 조작하므로 자기가 띄운 창만 다루고, 삭제·전송·결제처럼 되돌릴 수
135
- 없는 버튼은 누르지 않는다.
136
+ 없는 버튼은 누르지 않는다. 여러 단계 데스크톱 조작은 `computer_task`에 맡긴다(2026-10-27 사용자 결정). 목표·대상 창·
137
+ 완료 조건을 넘기면 `modelRoles.computer` 모델이 자식 세션에서 실행하고 관측 결과를 `DONE`·`NOT DONE`으로 돌려준다.
138
+ 한 번의 읽기 전용 관찰은 `computer`를 직접 써도 된다. 결과가 중요하면 보고를 믿지 말고 `windows_control list` 등으로
139
+ 따로 읽어 확인하고, 띄운 창은 PID를 확인해 자기 것만 닫는다. 이 규칙은 막는 게이트가 아니라 권고다.
136
140
  **테스트·검증은 사용자 화면 밖에서 돌린다(2026-10-26 사용자 결정).** 웹 테스트는 사용자 탭과 떨어진 독립
137
141
  공간(ego lite PC는 검증 프로필 TaskSpace, 그 밖은 headless)에서만 한다. 앱·Windows 프로그램 테스트는 사용자가
138
142
  보는 데스크톱에 창을 띄우지 않고 백그라운드에서 돌린다: 숨은 Windows 데스크톱(별도 desktop 객체), 화면 밖
@@ -158,7 +162,7 @@
158
162
  `HANDOFF.md`·`doc/README.md`·`doc/history` 기록을 만들어 같은 커밋에 넣는다.
159
163
  맞출 때는 남은 행동·관찰 조건·새 경계만 남기고, 이번에 끝난 행은 지운다. 그 자리에서 끝난 작업은
160
164
  HANDOFF가 아니라 `doc/history`에 기록한다(`rule://docs-handoff`).
161
- - 코드 변경이 high-risk이거나 트리거 marker·결정적 10% 표본에 해당하면 `git_finalize` 전에 `cross_review`로 다른 모델 계열(Anthropic 작성분은 OpenAI, OpenAI·그 밖 제공자 작성분은 Anthropic)의 검토를 받고 모든 finding을 근거와 함께 닫는다. 문서·테스트·생성물, 게이트가 주석만 바꿨다고 증명한 TS/JS 수정, git이 바이너리로 본 이미지·미디어·오피스·글꼴 자산은 제외한다. 타입만 바꾼 수정과 다른 언어의 주석 수정은 코드로 본다. 검토 대상은 바뀐 실행 코드·설정이고, 관련된 바뀌지 않은 호출부·계약은 `inputs`·`references`로 넘긴다. 그 밖의 일반 수정은 Main이 `maker_route` 검토 필요성 자문을 보고 정하며, 자문은 게이트 요구를 면제하지 못한다. 검토는 자문 증거이며 PASS가 아니다. 사용자의 명시 지시 없이 이 gate를 면제하지 않는다. 정본은 `harness-policy.json` `mainLane.workerReview.crossFamilyReview`다.
165
+ - 코드 변경이 high-risk이거나 트리거 marker·결정적 10% 표본에 해당하면 `git_finalize` 전에 `cross_review`로 다른 모델 계열(Anthropic 작성분은 OpenAI, OpenAI·그 밖 제공자 작성분은 Anthropic)의 검토를 받고 모든 finding을 근거와 함께 닫는다. 문서·테스트·생성물, 게이트가 파서로 주석·공백만 바꿨다고 증명한 TS/JS 수정, 버전 문자열(semver)만 바꾼 수정, git이 바이너리로 본 이미지·미디어·오피스·글꼴 자산은 제외한다. 타입만 바꾼 수정, 지시 주석 수정, 다른 언어의 주석 수정은 코드로 본다(VB 주석 줄은 marker 판정에서만 빠진다). high-risk 선언은 `crossReviewHighRisk`에 적은 파일에만 걸리고, 표본은 프로젝트·스타일·디자이너 파일을 빼고 바뀐 줄이 많은 코드 파일 5개까지만 요구한다(2026-10-27 사용자 결정). 검토 대상은 바뀐 실행 코드·설정이고, 관련된 바뀌지 않은 호출부·계약은 `inputs`·`references`로 넘긴다. 그 밖의 일반 수정은 Main이 `maker_route` 검토 필요성 자문을 보고 정하며, 자문은 게이트 요구를 면제하지 못한다. 검토는 자문 증거이며 PASS가 아니다. 사용자의 명시 지시 없이 이 gate를 면제하지 않는다. 정본은 `harness-policy.json` `mainLane.workerReview.crossFamilyReview`다.
162
166
  **검토 모델은 사용자가 정한 교차검증 경로만 쓴다(2026-10-26 사용자 지적).** 코드·문서·산출물의 검토, 검증, 게이트 판정에 쓰는 모델은 `config.yml` `modelRoles`의 `crossReviewOpenAI`·`crossReviewAnthropic` 슬롯(필수, `cross_review`와 같은 계열 규칙)과 자문 슬롯 `crossReviewAdvisory`(2026-10-26 Argon→Fable→AI Studio 프록시 Argon으로 교체, 저자가 그 모델 자체일 때만 빠짐)에서 읽는다. 새 검토 기능이나 Maker 지시서에 다른 모델(예: Sonnet)을 비용·속도를 이유로 직접 지정하지 않고, 그 경로 밖의 모델로 본 결과를 교차검증·검토 통과라고 부르지 않는다. 경로를 바꾸려면 먼저 사용자에게 묻는다(2026-10-26: Main이 슬롭 게이트 검토 모델을 Sonnet으로 지시서에 넣었다가 사용자가 바로잡았다).
163
167
  - `git_finalize`는 상대경로 canonical target만 받고 한 호출에서 저장소 하나만 마감한다. 경로
164
168
  해석과 거부 대상, cwd 밖 별도 Git 저장소 허용, Main의 raw Git fallback 4조건과 사전 확인,
@@ -3,10 +3,10 @@ name: maker
3
3
  description: Delegated end-to-end implementation agent that investigates, edits, repairs, validates its own slice, and verifies the actual surface it changed.
4
4
  model: "@implSonnet"
5
5
  thinking-level: auto
6
- tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, windows_control, ego_browser, checkpoint, cross_review, slop_check]
6
+ tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, windows_control, computer_task, ego_browser, checkpoint, cross_review, slop_check]
7
7
  ---
8
8
 
9
- <!-- omp-global-config:generated source-hash=95589f18419f
9
+ <!-- omp-global-config:generated source-hash=27baced8df24
10
10
  이 파일은 patches/build-agents.mjs 가 만든 빌드 산출물이다. 직접 수정하지 마라.
11
11
  고칠 곳: agent/sop/maker.md · agent/sop/_writer.md · agent/sop/_common.md
12
12
  재생성: node patches/build-agents.mjs 검사: node patches/build-agents.mjs --check
@@ -15,7 +15,9 @@ tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_im
15
15
  You are the delegated implementation Maker on the default `modelRoles.implSonnet` slot. A delegated slice
16
16
  is yours end to end.
17
17
 
18
- Publicly published/shared artifacts you write (README/CHANGELOG/docs and other public markdown, html, pptx/docx/xlsx/pdf/hwpx, report csv, UI copy packs) must pass `slop_check` before you report. Private/internal documents are excluded even when delivered as files to the user; code, tests and internal records such as `doc/history` are also excluded. Use the repository's publication declaration (`.publicignore` or `.slop-gate.json`); outside git, `public:true` may declare an otherwise unknown artifact public but never override a known private/internal exclusion. Unknown publication status does not trigger automatic review or a gate. Run the check on public files with your own model selector as `authors`, fix genuine findings and review changed content again (a pass is tied to exact bytes). Report false positives to Main with evidence; resolve is Main-only. If a reviewer response is malformed, inspect its raw response and parse error: do not retry unchanged input or alter artifact punctuation to work around the protocol failure. `git_finalize` and the outside-git turn-end gate apply only to public artifacts. Drafting and direct prose correction remain unchanged.
18
+ Every outward-facing artifact you write (README/CHANGELOG/docs and other markdown, html, pptx/docx/xlsx/pdf/hwpx, report csv, UI copy packs), including decks, reports and documents handed to a customer or the user as files, must pass `slop_check` before you report, whether or not it is published to a public repository and whether or not a `.publicignore`/`.slop-gate.json` declaration exists (user decision 2026-10-26, restated 2026-10-27). Only source-internal material is excluded: code, comments, logs, tests, fixtures, generated files, vendor, internal prompts/rules/SOP/skills, `HANDOFF.md`, `doc/history` and `doc/README.md`. Run the check with your own model selector as `authors`, fix genuine findings and review changed content again (a pass is tied to exact bytes). Report false positives to Main with evidence; resolve is Main-only. If a reviewer response is malformed, inspect its raw response and parse error: do not retry unchanged input or alter artifact punctuation to work around the protocol failure. `git_finalize` and the outside-git turn-end gate apply to every such artifact. Drafting and direct prose correction remain unchanged.
19
+
20
+ Draft coverage is per authored block, not per tool invocation. A draft for introductory paragraphs does not cover new explanations, table copy or slide prose you write yourself. Draft every new or paragraph-rewritten block before applying it; splitting edits or minifying HTML is not an exemption. Direct edits remain allowed for exact factual/typo/slop corrections and layout/code changes preserving the copy. New explanations, examples and conclusions are not corrections. Compare the prose being applied with the draft and draft the missing blocks only; a slop pass does not prove draft coverage.
19
21
 
20
22
  Run a `cross_review` `review` of your changed code files before you report when the review is required: the brief marks the slice high-risk or carries a plain cross-family review line, a trigger marker the `git_finalize` gate uses hits your changed lines or paths (process termination, deletion, destructive SQL, deploy/release/migration, credentials/auth, encoding, device I/O), or Main forwards a finalizer sample hit for your files. Other code changes are reviewed only when Main's brief asks for it; Main decides from the `maker_route` review-necessity advice. Targets are changed executable code and config; pass relevant unchanged callers or contracts as `inputs`/`references`, not as targets. Docs, tests, generated files, git-detected binary media/office assets and TS/JS edits the gate proves comment-only are not targets. Run it after your focused checks pass, with your own model selector as `authors` (never `"main"`), fix or refute each finding with evidence, use the probe round when a reviewer proposes one, and run a delta round on the same chain after fixes. Report the reviewId chain and a proposed status (`fixed`/`refuted`/`limit`) with an evidence locator for every finding; `resolve` is Main-only. Main does not start a duplicate review of your code: when Main forwards findings or later rounds, you run them.
21
23
 
@@ -0,0 +1,45 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
2
+ // The runtime package is rewritten to the host SDK path by the legacy-pi extension loader, so it is imported lazily below.
3
+ import { COMPUTER_ROLE, runComputerTask, spawnChild, type ComputerTaskDeps, type ComputerTaskInput, type ComputerTaskResult } from "./lib/computer-task";
4
+
5
+ /**
6
+ * computer_task: runs a multi-step desktop goal on the `modelRoles.computer` model in a child omp session and returns
7
+ * what it observed (2026-10-27 user decision; evidence doc/history/2026/10/27-terra-sol-eval/computer). Main and Makers
8
+ * call this for multi-step goals; a single read-only observation can still use `computer` directly. The tool drives the
9
+ * real desktop with the same boundary as `computer` (`tools.approval.computer`), and it is serialized per process.
10
+ */
11
+
12
+ async function resolveDeps(ctx: ExtensionContext): Promise<ComputerTaskDeps> {
13
+ const { findScopedSettings } = await import("@oh-my-pi/pi-coding-agent/config/settings");
14
+ return { selector: findScopedSettings(ctx.cwd)?.getModelRole(COMPUTER_ROLE), exe: process.env.OMP_EXE, env: process.env, spawn: spawnChild };
15
+ }
16
+
17
+ export function toolResult(result: ComputerTaskResult): { content: [{ type: "text"; text: string }]; isError?: true } {
18
+ return { content: [{ type: "text", text: result.text }], ...(result.isError ? { isError: true as const } : {}) };
19
+ }
20
+
21
+ export default function computerTask(pi: ExtensionAPI): void {
22
+ const z = pi.zod;
23
+ pi.registerTool({
24
+ name: "computer_task", label: "Computer Task", loadMode: "essential",
25
+ // Same boundary as `computer`: core consults `tools.approval.computer` (falling back to `computer_task`) for this tier.
26
+ approval: { tier: "exec", policyKey: "computer" },
27
+ description: "Hand ONE multi-step desktop (computer use) goal to a worker that runs on the modelRoles.computer model and get back what it observed. "
28
+ + "Use it for multi-step desktop work (launch, locate, set, read back); a single read-only observation can still use `computer` directly. "
29
+ + "`goal` is what to achieve, `target` names the only window or app it may touch (one you launched or the user named), `done` is the observable "
30
+ + "success condition it must read back after acting. It never presses irreversible buttons and touches nothing else. The result starts with "
31
+ + "`computer_task: DONE` or `computer_task: NOT DONE` and shows the resolved model, wall time, tool calls and tool errors; a done:false report, "
32
+ + "a timeout, an abort or a malformed answer is always NOT DONE (isError). Calls in one process run one at a time. `timeoutSec` defaults to 600 (10-1800). "
33
+ + "Verify independently when the outcome matters: this is the worker's report, not proof.",
34
+ parameters: z.object({
35
+ goal: z.string().describe("What to achieve on the desktop, step by step if needed."),
36
+ target: z.string().describe("The only window/app the worker may touch: one you launched or the user named."),
37
+ done: z.string().describe("The observable success condition to read back after acting."),
38
+ timeoutSec: z.number().optional().describe("Seconds before the worker is killed. Default 600, clamped to 10-1800."),
39
+ }) as never,
40
+ async execute(_id, params, signal, _onUpdate, ctx) {
41
+ const deps = await resolveDeps(ctx);
42
+ return toolResult(await runComputerTask(params as ComputerTaskInput, deps, signal ?? new AbortController().signal));
43
+ },
44
+ });
45
+ }
@@ -74,7 +74,7 @@ export function advisoryModelOf(ctx: ExtensionContext): string | undefined {
74
74
  return model ? `${model.provider}/${model.id}` : undefined;
75
75
  }
76
76
 
77
- function reviewDeps(ctx: ExtensionContext, signal: AbortSignal): ReviewDeps {
77
+ function reviewDeps(ctx: ExtensionContext, signal: AbortSignal, notify?: (text: string) => void): ReviewDeps {
78
78
  const path = ledgerPath();
79
79
  const ledger = createRoutingLedger(path);
80
80
  const current = ctx.models.current() ?? ctx.model;
@@ -84,7 +84,9 @@ function reviewDeps(ctx: ExtensionContext, signal: AbortSignal): ReviewDeps {
84
84
  cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), mainModel: current ? `${current.provider}/${current.id}` : undefined, advisoryModel: advisoryModelOf(ctx),
85
85
  exec: spawnExec(signal), readAll: () => readCrossReviewRecords(path), append: (record) => ledger.append(record),
86
86
  snapshotRoot: snapshotRoot(),
87
+ // 자문 호출은 review가 돌아온 뒤에도 기한까지 돈다. ctx는 그 세션의 것이라 비용 장부도 같은 세션에 남는다.
87
88
  complete: (family, prompt, attempt) => completeReviewer(family, prompt, ctx, attempt),
89
+ ...(notify ? { notify } : {}),
88
90
  };
89
91
  }
90
92
 
@@ -104,8 +106,8 @@ export interface CrossReviewParams {
104
106
  unreviewedAcknowledgements?: { path: string; evidence: string }[];
105
107
  }
106
108
 
107
- /** 진행 알림을 받는 도구 update 콜백(core의 onUpdate와 같은 모양). */
108
- export type CrossReviewUpdate = (partial: { content: { type: "text"; text: string }[]; details: { progress: ReviewProgress } }) => void;
109
+ /** 진행 알림을 받는 도구 update 콜백(core의 onUpdate와 같은 모양). review는 reviewer 상태, resolve는 자문 대기 한 줄을 보낸다. */
110
+ export type CrossReviewUpdate = (partial: { content: { type: "text"; text: string }[]; details: { progress: ReviewProgress } | { resolveProgress: string } }) => void;
109
111
 
110
112
  export async function executeCrossReview(
111
113
  params: CrossReviewParams, ctx: ExtensionContext, signal: AbortSignal,
@@ -142,7 +144,7 @@ export async function executeCrossReview(
142
144
  if (params.action === "resolve") {
143
145
  if (!params.reviewId) throw new Error("resolve requires reviewId.");
144
146
  const input: ResolveInput = { reviewId: params.reviewId, resolutions: params.resolutions ?? [], unreviewedAcknowledgements: params.unreviewedAcknowledgements };
145
- return runResolve(input, deps);
147
+ return runResolve(input, onUpdate ? { ...deps, onProgress: (text) => onUpdate({ content: [{ type: "text", text }], details: { resolveProgress: text } }) } : deps);
146
148
  }
147
149
  throw new Error(`Unknown action ${JSON.stringify(params.action)}; use "review" or "resolve".`);
148
150
  }
@@ -151,7 +153,7 @@ export default function crossReview(pi: ExtensionAPI): void {
151
153
  const z = pi.zod;
152
154
  pi.registerTool({
153
155
  name: "cross_review", label: "Cross Review", loadMode: "essential", approval: "read",
154
- description: "계열 교차검증. review는 Main과 Maker가 부를 수 있고 resolve는 Main 전용이다(Maker는 자기 high-risk 코드의 review 체인을 돌린 뒤 reviewId 체인과 finding별 제안 status·근거를 보고하고, Main이 확인해 resolve한다). action=review: files(cwd 기준 상대 경로)의 변경을 반대 계열 reviewer(Anthropic 저자→OpenAI 슬롯 Astra, OpenAI·그 밖의 저자→Anthropic 슬롯 Opus)와 늘 함께 도는 자문 슬롯 crossReviewAdvisory(Argon, 저자가 그 모델 자체일 때만 빠진다)가 acceptance 기준으로 적대적으로 읽고 {reviewId, reviewer 모델, round, triggers, findings[]}를 돌려준다. 자문 reviewer는 필수 reviewer와 함께 시작해 120초가 지나거나 필수 reviewer가 모두 끝나고 20초 유예가 지나면 끊기며 호출 실패·해석 불가·시간 초과·중단은 reviewers[]와 advisoryReviewers에 기록만 하고 통과를 막지 않지만(다음 라운드가 다시 부른다), 그 전에 답한 finding(R*-ADV-*, 옛 기록은 R*-GGL-*)은 다른 reviewer의 것처럼 resolve로 닫아야 한다. authors는 코드를 쓴 모델 selector이며 두 계열이면 둘 다 돌린다. Main 세션은 \"main\"(자기 현재 모델)도 쓸 수 있지만 subagent 세션에서 \"main\"은 거절되므로 자기 모델 selector를 주고, subagent 세션의 1회차 authors에 세션 자신의 모델과 같은 계열이 없으면 거절된다. previousReviewId를 주면 다음 라운드이고 체인은 최대 5회(probe 라운드 1회 + probe가 아닌 라운드 4회)다: probe가 아닌 라운드는 직전 라운드가 본 내용 이후의 delta만 보며 바뀐 것이 없으면 거절한다. finding을 고친 뒤에는 다음 라운드로 그 수정을 reviewer에게 다시 보이는 것이 정상 흐름이며, 마지막 라운드의 내용과 다른 파일이 resolve 때 있으면 결과의 unreviewedChanges로 알리며, 그 중 코드 파일(git_finalize 게이트와 같은 분류)은 통과를 막는다(문서·테스트·생성물만이면 안내일 뿐 통과 판정은 그대로다). base(선택, 1회차 전용, 빈 문자열은 주지 않은 것으로 읽는다)는 이미 커밋된 변경을 심사할 때 쓰는 HEAD 또는 그 조상 커밋이며, 그 커밋부터 HEAD까지의 변경을 보여 준다. 대상 파일의 작업 트리가 HEAD와 같아야 하고(수정·삭제·추적 안 한 새 파일은 거절, git이 무시하는 새 파일은 건너뜀) base가 없으면 HEAD와 작업 트리를 비교한다(커밋 뒤에도 같은 체인을 previousReviewId로 이어 가는 것은 base 없이 된다). reviewer는 코드만으로 가릴 수 없는 의심에 finding.probe{steps, expectIfReal, expectIfNot?}(재현 실험)를 붙일 수 있다. Main이나 Maker가 그 실험을 돌린 뒤 previousReviewId와 probeResults[{findingId, command, exitCode, output}]를 주면 probe 라운드가 되어 코드 변경이 없어도 실험 결과로 한 번 더 판단받고, reviewer가 finding마다 verdicts{confirmed|withdrawn|unresolved}를 낸다(판단이 빠지면 unresolved). 체인에 없는 findingId·중복 결과·두 번째 probe 라운드·개인키 블록이 든 결과는 거절하며, output은 발췌로 결과당 8KB(넘으면 앞뒤만 남김)·합계 40KB(넘으면 거절)이고 비밀 값은 가려 신뢰하지 않는 자료로 전달한다. verdict는 자문일 뿐 finding을 자동으로 닫거나 통과시키지 않는다. references(선택, 최대 10개)는 심사 대상이 아닌 맥락용 기존·신규 파일(예: 작업 기록, 계약 파일)의 현재 내용을 변경 뒤에 읽기 전용으로 싣는다. 명시한 경로만이며 통과 지문을 덮지 않고 references만으로는 심사하지 않는다. 정책상 제외 경로(비밀을 담는 파일)·gitignore·심볼릭 링크·바이너리·파일당 64KB/합계 100KB 초과, 읽는 동안 열린 파일의 실제 경로가 고정한 저장소 루트 아래 요청 경로와 달라진 경우는 빠지고 skippedReferences에 이유가 나온다. 이 경로 확인은 Linux(WSL 포함)의 /proc/self/fd에 의존하므로 native Windows/macOS에서는 references를 읽지 않고 skippedReferences에 unsupported 이유를 남긴다(변경 파일 심사는 그대로). inputs(선택, 최대 10개)는 변경이 일관돼야 하는 명세·계약·스키마·호출자 파일이며, references와 같은 읽기·필터·제한으로 별도 블록에 싣고(심사 대상 아님, 빠진 것은 skippedInputs) files와 겹치면 거절한다. references와 달리 통과에 묶여서, resolve 뒤 입력이 바뀌거나 지워지면 git_finalize가 그 통과를 덮지 않는 것으로 보고 다시 검토를 요구한다(inputs 없는 검토는 그대로). 이어지는 라운드는 앞 라운드의 입력을 이어받고 더할 수 있으며, 이어받은 입력을 더 읽을 수 없으면 거절하므로 빼려면 previousReviewId 없이 새로 시작한다. 파일 내용은 reviewer 슬롯이 가리키는 provider(현재 OpenAI·Anthropic, 자문 자리는 Google AI Studio 프록시)로 전송된다. 비밀을 담는 파일(.env*, 키·인증서 형식, id_*, agent.db, .npmrc·.netrc·.pypirc·.git-credentials, ~/.omp/agent 아래)과 개인키 블록이 든 파일, 바이너리, 큰 입력은 제외한다. 제외한 파일은 정책상 심사할 수 없다는 이유와 다음 단계(이름·위치 바꾸기, 커밋에서 빼기, 사용자 명시 지시의 waiver)를 결과 skipped·nextStep에 바로 알리며, 같은 파일에 라운드를 더 돌려도 통과는 나오지 않고 git_finalize의 거절 메시지도 같은 이유를 말한다. credential·secret·password·api-key를 품은 이름이나 단독 auth·token 이름처럼 이름만 걸린 파일은 제외하지 않고 비밀로 보이는 값을 [REDACTED]로 가려(결과 redacted에 횟수) 심사한다. 이미 시작된 심사에서 바뀐 줄이 테스트 파일 삭제·테스트 호출 순감소·skip 추가·억제 주석 추가·빈 catch 추가 모양이면 결과의 hidingLeads와 reviewer 프롬프트에 판정이 아닌 단서로만 붙으며 trigger·표본·게이트 판정은 바뀌지 않는다. action=resolve(Main 전용, 다른 세션이 시작한 체인도 닫을 수 있다): reviewId 체인의 모든 finding에 {id, status fixed|refuted|limit, evidence}가 있어야 pass:true이고 그 시점 파일 내용(git blob)이 통과 지문으로 기록된다(withdrawn 판단도 자동으로 닫지 않는다). 마지막 라운드 reviewer가 본 내용과 다른 코드 파일이 resolve 시점에 남아 있으면 finding이 모두 닫혀도 pass:false(unreviewedBlocking)이고, previousReviewId로 새 라운드를 돌려 그 수정을 reviewer에게 보인 뒤 다시 resolve한다. 더 돌릴 probe가 아닌 라운드가 없는 체인(또는 마지막 라운드가 바이너리·개인키·정책상 제외 경로로 건너뛴 파일)에서만 unreviewedAcknowledgements[{path, evidence}]로 미검토 코드 파일마다 그 내용에 한 검사를 근거로 적어 통과할 수 있고(라운드가 남았거나 근거가 비면 rejectedAcknowledgements로 거절), 받아들인 확인 응답은 resolve 기록에 경로·blob·근거로 남는다. git_finalize는 trigger·표본에 걸린 코드 파일에 이 통과 기록을 요구한다(주석 줄만 바뀐 것이 증명된 TS/JS 수정과 git이 바이너리로 보는 미디어·문서·글꼴 자산은 대상이 아니다). reviewer의 PASS 표현은 근거가 아니다.",
156
+ description: "계열 교차검증. review는 Main과 Maker가 부를 수 있고 resolve는 Main 전용이다(Maker는 자기 high-risk 코드의 review 체인을 돌린 뒤 reviewId 체인과 finding별 제안 status·근거를 보고하고, Main이 확인해 resolve한다). action=review: files(cwd 기준 상대 경로)의 변경을 반대 계열 reviewer(Anthropic 저자→OpenAI 슬롯 Astra, OpenAI·그 밖의 저자→Anthropic 슬롯 Opus)와 늘 함께 도는 자문 슬롯 crossReviewAdvisory(Argon, 저자가 그 모델 자체일 때만 빠진다)가 acceptance 기준으로 적대적으로 읽고 {reviewId, reviewer 모델, round, triggers, findings[]}를 돌려준다. 결과는 필수 reviewer가 모두 끝나면 바로 돌아온다. 그때 아직 답하지 않은 자문 reviewer는 reviewers[]에 pending(advisoryPending 안내)으로 남고 뒤에서 자기 시작부터 600초까지 계속 돌며, 답한 finding(R<n>-ADV-*)은 원장에 그 라운드의 열린 지적으로 붙고 세션에 알림이 오며 다음 라운드 프롬프트와 earlierAdvisoryFindings에 보인다. 필수 reviewer가 쓸 수 없는 답을 내면 자문은 멈춘다. 자문의 호출 실패·해석 불가·시간 초과·중단은 기록만 하고 통과를 막지 않지만(다음 라운드가 다시 부른다), 답한 finding(옛 기록은 R*-GGL-*)은 다른 reviewer의 것처럼 resolve로 닫아야 한다. authors는 코드를 쓴 모델 selector이며 두 계열이면 둘 다 돌린다. Main 세션은 \"main\"(자기 현재 모델)도 쓸 수 있지만 subagent 세션에서 \"main\"은 거절되므로 자기 모델 selector를 주고, subagent 세션의 1회차 authors에 세션 자신의 모델과 같은 계열이 없으면 거절된다. previousReviewId를 주면 다음 라운드이고 체인은 최대 5회(probe 라운드 1회 + probe가 아닌 라운드 4회)다: probe가 아닌 라운드는 직전 라운드가 본 내용 이후의 delta만 보며 바뀐 것이 없으면 거절한다. finding을 고친 뒤에는 다음 라운드로 그 수정을 reviewer에게 다시 보이는 것이 정상 흐름이며, 마지막 라운드의 내용과 다른 파일이 resolve 때 있으면 결과의 unreviewedChanges로 알리며, 그 중 코드 파일(git_finalize 게이트와 같은 분류)은 통과를 막는다(문서·테스트·생성물만이면 안내일 뿐 통과 판정은 그대로다). base(선택, 1회차 전용, 빈 문자열은 주지 않은 것으로 읽는다)는 이미 커밋된 변경을 심사할 때 쓰는 HEAD 또는 그 조상 커밋이며, 그 커밋부터 HEAD까지의 변경을 보여 준다. 대상 파일의 작업 트리가 HEAD와 같아야 하고(수정·삭제·추적 안 한 새 파일은 거절, git이 무시하는 새 파일은 건너뜀) base가 없으면 HEAD와 작업 트리를 비교한다(커밋 뒤에도 같은 체인을 previousReviewId로 이어 가는 것은 base 없이 된다). reviewer는 코드만으로 가릴 수 없는 의심에 finding.probe{steps, expectIfReal, expectIfNot?}(재현 실험)를 붙일 수 있다. Main이나 Maker가 그 실험을 돌린 뒤 previousReviewId와 probeResults[{findingId, command, exitCode, output}]를 주면 probe 라운드가 되어 코드 변경이 없어도 실험 결과로 한 번 더 판단받고, reviewer가 finding마다 verdicts{confirmed|withdrawn|unresolved}를 낸다(판단이 빠지면 unresolved). 체인에 없는 findingId·중복 결과·두 번째 probe 라운드·개인키 블록이 든 결과는 거절하며, output은 발췌로 결과당 8KB(넘으면 앞뒤만 남김)·합계 40KB(넘으면 거절)이고 비밀 값은 가려 신뢰하지 않는 자료로 전달한다. verdict는 자문일 뿐 finding을 자동으로 닫거나 통과시키지 않는다. references(선택, 최대 10개)는 심사 대상이 아닌 맥락용 기존·신규 파일(예: 작업 기록, 계약 파일)의 현재 내용을 변경 뒤에 읽기 전용으로 싣는다. 명시한 경로만이며 통과 지문을 덮지 않고 references만으로는 심사하지 않는다. 정책상 제외 경로(비밀을 담는 파일)·gitignore·심볼릭 링크·바이너리·파일당 64KB/합계 100KB 초과, 읽는 동안 열린 파일의 실제 경로가 고정한 저장소 루트 아래 요청 경로와 달라진 경우는 빠지고 skippedReferences에 이유가 나온다. 이 경로 확인은 Linux(WSL 포함)의 /proc/self/fd에 의존하므로 native Windows/macOS에서는 references를 읽지 않고 skippedReferences에 unsupported 이유를 남긴다(변경 파일 심사는 그대로). inputs(선택, 최대 10개)는 변경이 일관돼야 하는 명세·계약·스키마·호출자 파일이며, references와 같은 읽기·필터·제한으로 별도 블록에 싣고(심사 대상 아님, 빠진 것은 skippedInputs) files와 겹치면 거절한다. references와 달리 통과에 묶여서, resolve 뒤 입력이 바뀌거나 지워지면 git_finalize가 그 통과를 덮지 않는 것으로 보고 다시 검토를 요구한다(inputs 없는 검토는 그대로). 이어지는 라운드는 앞 라운드의 입력을 이어받고 더할 수 있으며, 이어받은 입력을 더 읽을 수 없으면 거절하므로 빼려면 previousReviewId 없이 새로 시작한다. 파일 내용은 reviewer 슬롯이 가리키는 provider(현재 OpenAI·Anthropic, 자문 자리는 Google AI Studio 프록시)로 전송된다. 비밀을 담는 파일(.env*, 키·인증서 형식, id_*, agent.db, .npmrc·.netrc·.pypirc·.git-credentials, ~/.omp/agent 아래)과 개인키 블록이 든 파일, 바이너리, 큰 입력은 제외한다. 제외한 파일은 정책상 심사할 수 없다는 이유와 다음 단계(이름·위치 바꾸기, 커밋에서 빼기, 사용자 명시 지시의 waiver)를 결과 skipped·nextStep에 바로 알리며, 같은 파일에 라운드를 더 돌려도 통과는 나오지 않고 git_finalize의 거절 메시지도 같은 이유를 말한다. credential·secret·password·api-key를 품은 이름이나 단독 auth·token 이름처럼 이름만 걸린 파일은 제외하지 않고 비밀로 보이는 값을 [REDACTED]로 가려(결과 redacted에 횟수) 심사한다. 이미 시작된 심사에서 바뀐 줄이 테스트 파일 삭제·테스트 호출 순감소·skip 추가·억제 주석 추가·빈 catch 추가 모양이면 결과의 hidingLeads와 reviewer 프롬프트에 판정이 아닌 단서로만 붙으며 trigger·표본·게이트 판정은 바뀌지 않는다. action=resolve(Main 전용, 다른 세션이 시작한 체인도 닫을 수 있다): 이 프로세스에서 체인의 자문 reviewer가 아직 돌면 진행 알림을 내며 그 끝(최대 600초)을 기다린 뒤 판정하고, 진행 중 호출도 완료 기록도 없는 pending 자문(재시작 등)은 실패로 기록하고 막지 않는다. reviewId 체인의 모든 finding(늦게 붙은 자문 finding 포함)에 {id, status fixed|refuted|limit, evidence}가 있어야 pass:true이고 그 시점 파일 내용(git blob)이 통과 지문으로 기록된다(withdrawn 판단도 자동으로 닫지 않는다). 마지막 라운드 reviewer가 본 내용과 다른 코드 파일이 resolve 시점에 남아 있으면 finding이 모두 닫혀도 pass:false(unreviewedBlocking)이고, previousReviewId로 새 라운드를 돌려 그 수정을 reviewer에게 보인 뒤 다시 resolve한다. 더 돌릴 probe가 아닌 라운드가 없는 체인(또는 마지막 라운드가 바이너리·개인키·정책상 제외 경로로 건너뛴 파일)에서만 unreviewedAcknowledgements[{path, evidence}]로 미검토 코드 파일마다 그 내용에 한 검사를 근거로 적어 통과할 수 있고(라운드가 남았거나 근거가 비면 rejectedAcknowledgements로 거절), 받아들인 확인 응답은 resolve 기록에 경로·blob·근거로 남는다. git_finalize는 crossReviewHighRisk로 선언한 파일·trigger·표본(커밋당 바뀐 줄이 많은 코드 파일 최대 5개, 프로젝트·스타일·디자이너 파일 제외)에 걸린 코드 파일에 이 통과 기록을 요구한다. TypeScript 파서로 주석만 바뀐 것이 증명된 TS/JS 수정, HEAD나 통과한 내용과 비교해 버전 문자열만 바뀐 수정, git이 바이너리로 보는 미디어·문서·글꼴 자산은 대상이 아니다. reviewer의 PASS 표현은 근거가 아니다.",
155
157
  parameters: z.object({
156
158
  action: z.string(),
157
159
  files: z.array(z.string()).optional(),
@@ -168,7 +170,11 @@ export default function crossReview(pi: ExtensionAPI): void {
168
170
  unreviewedAcknowledgements: z.array(z.object({ path: z.string(), evidence: z.string() })).optional(),
169
171
  }) as never,
170
172
  async execute(_id, params, signal, onUpdate, ctx) {
171
- const result = await executeCrossReview(params as CrossReviewParams, ctx, signal ?? new AbortController().signal, reviewDeps, onUpdate as CrossReviewUpdate | undefined);
173
+ // 늦게 끝난 자문 finding은 세션에 알린다: 도는 중이면 다음 단계 경계에, 쉬는 중이면 대화에 바로 보이게(턴을 시작하지 않는다) 남긴다.
174
+ const notify = (text: string) => {
175
+ pi.sendMessage({ customType: "cross-review-advisory", content: text, display: true, attribution: "agent" }, ctx.isIdle?.() ? undefined : { deliverAs: "aside" });
176
+ };
177
+ const result = await executeCrossReview(params as CrossReviewParams, ctx, signal ?? new AbortController().signal, (c, s) => reviewDeps(c, s, notify), onUpdate as CrossReviewUpdate | undefined);
172
178
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
173
179
  },
174
180
  });
@@ -0,0 +1,146 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
4
+ import { onSessionLifecycle } from "./lib/session-identity";
5
+ import { candidatesOf } from "./lib/slop-gate";
6
+ import { coverage, COVERED_RATIO, denyText, inScope, lettersOf, MIN_UNCOVERED_LETTERS, proseChanges, reminderText, shingles, uncoveredBlocks } from "./lib/draft-coverage";
7
+
8
+ /**
9
+ * write·edit가 글 파일을 바꾸기 전에, draft를 거치지 않은 새 글 덩어리가 문단 이상 쌓이면 그 호출을 한 번 멈추고 빠진 덩어리를 보여 준다.
10
+ * 판단 근거는 lib/draft-coverage.ts의 글자 4-gram 유사도이며 저자 증명이 아니다(Main 결정 2026-10-27: 쓰기 전 한정 개입, 출처 게이트 아님).
11
+ * - draft 결과는 성공한 tool_result 순간의 내용을 고정해 담는다(나중에 draft 파일을 손으로 고쳐도 덮는 범위가 늘지 않는다).
12
+ * 같은 프로세스의 다른 세션(Main·Maker)이 만든 결과는 이 세션이 그 경로를 read한 뒤에만 쓴다.
13
+ * - 바뀌기 전 내용(기준선)은 사용자 입력마다 파일별로 처음 손댈 때 고정한다: 잘게 나눠 직접 더한 글이 기준선으로 씻기지 않는다.
14
+ * - 같은 파일·같은 덩어리 묶음을 그대로 다시 보내면 통과시킨다(예외를 밝힌 경우의 복구 길). 통과는 덮음으로 기록하지 않는다.
15
+ * - bash·eval이 바꾼 글 파일은 내용을 볼 수 없어 알림만 한다. docx·xlsx·pdf draft 결과와 그 밖의 도구가 쓴 글은 범위 밖이다.
16
+ */
17
+
18
+ const MAX_DRAFTS = 64;
19
+ const MAX_TEXT_BYTES = 4 * 1024 * 1024;
20
+ const SNAPSHOT_FORMAT = /\.(?:md|markdown|txt|csv)$/i;
21
+
22
+ interface DraftSnapshot { path: string; owner: object; grams: Set<string> }
23
+ // 같은 프로세스의 세션(Main·in-process Maker)이 함께 보는 성공 draft 결과. 같은 경로에 다시 draft해도 앞 결과를 지우지 않는다.
24
+ // 오래된 것부터 지워 크기를 묶는다.
25
+ const drafts: DraftSnapshot[] = [];
26
+
27
+ function readText(path: string): string | undefined {
28
+ try {
29
+ if (statSync(path).size > MAX_TEXT_BYTES) return undefined;
30
+ return readFileSync(path, "utf8").replace(/^\uFEFF/, "");
31
+ } catch { return undefined; }
32
+ }
33
+
34
+ const textOf = (content: unknown) => (Array.isArray(content)
35
+ ? content.map((part: unknown) => (part && typeof part === "object" && "text" in part && typeof part.text === "string" ? part.text : "")).join("\n")
36
+ : "");
37
+
38
+ export default function draftCoverage(pi: ExtensionAPI): void {
39
+ // 세션이 바뀌면 소유 표지도 바꾼다: 앞 세션의 draft는 read해야만 쓸 수 있는 다른 세션의 결과가 된다.
40
+ let owner = {};
41
+ // 쫓겨난 스냅숏을 붙잡지 않도록 약한 참조로 둔다.
42
+ let granted = new WeakSet<DraftSnapshot>();
43
+ let baselines = new Map<string, Set<string>>();
44
+ let pending = new Map<string, Map<string, string>>();
45
+ let acknowledged = new Map<string, Set<string>>();
46
+ let denied = new Set<string>();
47
+ let reminded = new Set<string>();
48
+ let started = new Map<string, number>();
49
+ // 통과시킨 write·edit의 기록은 그 도구가 성공한 뒤에만 남긴다(실패한 시도는 더한 글이 아니다).
50
+ let staged = new Map<string, (() => void)[]>();
51
+ const resetInput = () => {
52
+ baselines = new Map(); pending = new Map(); acknowledged = new Map(); denied = new Set(); reminded = new Set(); started = new Map(); staged = new Map();
53
+ };
54
+ const resetSession = () => { owner = {}; granted = new WeakSet(); resetInput(); };
55
+
56
+ onSessionLifecycle(pi, { start: resetSession });
57
+ pi.on("session_shutdown", resetSession);
58
+ pi.on("input", (event) => {
59
+ if (event.source === "interactive" || event.source === "rpc") resetInput();
60
+ });
61
+
62
+ pi.on("tool_call", (event, ctx) => {
63
+ const input = (event.input ?? {}) as Record<string, unknown>;
64
+ if (event.toolName === "bash" || event.toolName === "eval") { started.set(event.toolCallId, Date.now()); return; }
65
+ const changes = proseChanges(event.toolName, input, ctx.cwd);
66
+ if (changes.length === 0) return;
67
+ const usable = drafts.filter((snap) => snap.owner === owner || granted.has(snap)).map((snap) => snap.grams);
68
+ const reasons: string[] = [];
69
+ const accepted: (() => void)[] = [];
70
+ for (const change of changes) {
71
+ let baseline = baselines.get(change.path);
72
+ if (!baseline) {
73
+ baseline = shingles(readText(change.path) ?? "");
74
+ baselines.set(change.path, baseline);
75
+ }
76
+ const acked = acknowledged.get(change.path) ?? new Set<string>();
77
+ const fresh = new Map<string, string>();
78
+ for (const block of uncoveredBlocks(change.added, change.path, [baseline, ...usable])) {
79
+ const key = [...shingles(block)].join("");
80
+ if (!acked.has(key)) fresh.set(key, block);
81
+ }
82
+ // write는 파일 전체를 다시 쓰므로 그 파일의 쌓인 덩어리를 이번 내용으로 바꾸고, edit는 더한다. 쌓인 덩어리도 지금 쓸 수 있는 draft로 다시 본다.
83
+ const sources = [baseline, ...usable];
84
+ const before = event.toolName === "write" ? [] : [...pending.get(change.path) ?? []].filter(([, block]) => coverage(block, sources) < COVERED_RATIO);
85
+ const candidate = new Map([...before, ...fresh]);
86
+ const blocks = [...candidate.values()];
87
+ const key = `${change.path}\n${[...candidate.keys()].sort().join("\n")}`;
88
+ if (fresh.size > 0 && lettersOf(blocks) >= MIN_UNCOVERED_LETTERS && !denied.has(key)) {
89
+ denied.add(key);
90
+ reasons.push(denyText(change.path, blocks));
91
+ continue;
92
+ }
93
+ // 성공한 뒤의 최신 상태에 합친다: 같은 파일의 호출이 겹쳐 끝나도 앞 호출이 더한 덩어리를 덮어쓰지 않는다.
94
+ accepted.push(() => {
95
+ const latest = pending.get(change.path) ?? new Map<string, string>();
96
+ if (denied.has(key)) {
97
+ // 같은 묶음을 그대로 다시 보냈다: 예외로 받아들인 덩어리는 다시 묻지 않는다. 덮음으로 기록하지는 않는다.
98
+ const acks = acknowledged.get(change.path) ?? new Set<string>();
99
+ for (const k of candidate.keys()) { acks.add(k); latest.delete(k); }
100
+ acknowledged.set(change.path, acks);
101
+ pending.set(change.path, latest);
102
+ } else pending.set(change.path, event.toolName === "write" ? candidate : new Map([...latest, ...fresh]));
103
+ });
104
+ }
105
+ if (reasons.length > 0) return { block: true, reason: reasons.join("\n\n") };
106
+ if (accepted.length > 0) staged.set(event.toolCallId, accepted);
107
+ });
108
+
109
+ pi.on("tool_result", (event, ctx) => {
110
+ const input = (event.input ?? {}) as Record<string, unknown>;
111
+ if (event.toolName === "write" || event.toolName === "edit") {
112
+ const apply = staged.get(event.toolCallId);
113
+ staged.delete(event.toolCallId);
114
+ if (!event.isError) for (const step of apply ?? []) step();
115
+ return;
116
+ }
117
+ if (event.toolName === "draft") {
118
+ if (event.isError || typeof input.output !== "string" || textOf(event.content).startsWith("model: none")) return;
119
+ const path = resolve(ctx.cwd, input.output);
120
+ if (!SNAPSHOT_FORMAT.test(path)) return;
121
+ const text = readText(path);
122
+ if (text === undefined) return;
123
+ drafts.push({ path, owner, grams: shingles(text) });
124
+ if (drafts.length > MAX_DRAFTS) drafts.splice(0, drafts.length - MAX_DRAFTS);
125
+ return;
126
+ }
127
+ if (event.toolName === "read") {
128
+ if (event.isError || typeof input.path !== "string") return;
129
+ // read 경로 뒤의 줄 범위·:raw 선택자는 떼고 맞춘다. 이 세션이 실제로 연 draft 결과만 쓸 수 있게 된다.
130
+ const bare = input.path.replace(/(?::(?:raw|conflicts|img|-?\d[\d+,:-]*(?::raw)?))+$/, "");
131
+ const path = isAbsolute(bare) ? bare : resolve(ctx.cwd, bare);
132
+ for (const snap of drafts) if (snap.path === path) granted.add(snap);
133
+ return;
134
+ }
135
+ if (event.toolName !== "bash" && event.toolName !== "eval") return;
136
+ const since = (started.get(event.toolCallId) ?? Date.now()) - 2_000;
137
+ started.delete(event.toolCallId);
138
+ const changed = candidatesOf(event.toolName, input, ctx.cwd).files.filter((path) => {
139
+ if (!inScope(path, ctx.cwd) || reminded.has(path)) return false;
140
+ try { return statSync(path).mtimeMs >= since; } catch { return false; }
141
+ });
142
+ if (changed.length === 0) return;
143
+ for (const path of changed) reminded.add(path);
144
+ pi.sendMessage({ customType: "draft-coverage", content: reminderText(changed), display: false, attribution: "agent" }, { deliverAs: "aside" });
145
+ });
146
+ }