zigmini 1.0.0-beta.1 → 1.0.0-beta.2

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/README.ko.md CHANGED
@@ -195,6 +195,13 @@ F0:AB:B3:B2:DC:70:1A:45:19:85:69:7D:58:68:F5:FB:4F:AC:33:4F:D8:6A:68:DD:04:5C:53
195
195
 
196
196
  사용자 매뉴얼([한국어](docs/manual.ko.md), [English](docs/manual.md))은 웹 설정 창 위쪽의 "매뉴얼" 버튼과, 앱 헤더의 상태 태그를 짧게 눌러 여는 연결 화면의 "매뉴얼" 버튼으로 봅니다.
197
197
 
198
+ ## 1.0.0-beta.2의 새 기능
199
+
200
+ 1.0.0-beta.2(2026-11-03 KST, Android `versionCode` 31)는 핫픽스입니다. 엔진은 바뀌지 않습니다(`5.1.51-zgw.3`). 전체 노트: [docs/releases/1.0.0-beta.2.ko.md](docs/releases/1.0.0-beta.2.ko.md).
201
+
202
+ - **보안검사:** 보안 기준에 용어를 추가하지 않아도 `zigmini scan`이 동작합니다(버그 268).
203
+ - **에이전트 규칙:** 공통 규칙 항목마다 고정 ID가 있습니다. `~/.zigmini/rules.md`의 항목에 `<!--replaces:<id>-->`를 붙이면 그 공통 항목을 대신합니다. `zigmini rules check`로 파일을 확인합니다.
204
+
198
205
  ## 1.0.0-beta.1의 새 기능
199
206
 
200
207
  1.0.0-beta.1(2026-11-02 KST, Android `versionCode` 30)은 1.0.0의 첫 후보입니다. 엔진 `5.1.51-zgw` 개정 3(OMO 5.1.51, 불변 빌드 `5.1.51-zgw.3`, senpi 2026.10.27-1, 대화 스키마 10)을 선택합니다. 전체 노트는 [docs/releases/1.0.0-beta.1.ko.md](docs/releases/1.0.0-beta.1.ko.md)에 있습니다.
@@ -320,6 +327,11 @@ zigmini update <버전> # 지정한 버전으로 업데이트
320
327
  - 패키지의 `assets/agent/gateway-rules.md`: 모든 설치에 공통인 규칙입니다. 업데이트가 바꿉니다.
321
328
  - `~/.zigmini/rules.md`: 이 설치에만 쓰는 규칙(예: 호칭, 언어, 계정)입니다. 사용자가 직접 고칩니다. 두 파일이 부딪치면 이 파일이 이깁니다.
322
329
 
330
+ 공통 규칙 항목 바꾸기: 공통 규칙 파일의 각 항목에는 주석으로 고정 ID가 있습니다(예: `<!--id:f2468e-->`). ID는 항목을 만들 때 한 번 정하고, 문장을 고쳐도 바뀌지 않습니다. 항목 번호는 읽는 순서일 뿐이고 우선순위가 아닙니다. 공통 항목 하나를 통째로 바꾸려면 `rules.md`의 항목 안에 `<!--replaces:<ID>-->`를 적습니다. 그러면 게이트웨이는 그 공통 항목을 빼고 `rules.md`의 항목을 씁니다. `replaces`가 없는 항목은 지금처럼 새 규칙으로 공통 규칙 뒤에 붙고, 부딪치면 `rules.md`가 이깁니다. 적은 ID가 공통 규칙에 없으면(예: 업데이트로 사라짐) 그 항목은 새 규칙으로 붙고 `zigmini doctor`가 경고를 보여 줍니다.
331
+
332
+ - `zigmini rules`: 공통 항목의 번호, ID, 첫 줄을 보여 줍니다.
333
+ - `zigmini rules check`: `rules.md`의 각 항목이 "공통 N번을 대체", "신규 규칙", "없는 ID" 중 무엇인지 보여 줍니다(없는 ID가 있으면 종료 코드 1).
334
+
323
335
  바꾼 뒤에 열리는 세션부터 새 내용을 씁니다. 바로 적용하려면 쉬는 때 `zigmini restart bridge`를 실행합니다.
324
336
 
325
337
  브리지는 차례마다 모든 세션에 프로필 닉네임도 알려 줍니다. 그래서 닉네임을 위한 규칙은 필요하지 않습니다. main, 작업, 에이전트 세션이 모두 받습니다. main은 기본 에이전트 이름도 받고, 각 에이전트는 만들거나 편집할 때 정한 자기 이름을 받습니다. 바꾼 닉네임과 이름은 다음 차례부터 적용됩니다. 닉네임이 기본값("Me" 또는 "나")이면 세션은 닉네임이 없다는 안내를 받습니다.
package/README.md CHANGED
@@ -195,6 +195,13 @@ F0:AB:B3:B2:DC:70:1A:45:19:85:69:7D:58:68:F5:FB:4F:AC:33:4F:D8:6A:68:DD:04:5C:53
195
195
 
196
196
  The user manual ([English](docs/manual.md), [한국어](docs/manual.ko.md)) opens from the "매뉴얼" (Manual) button at the top of the web settings dialog, and in the app from the "매뉴얼" button on the connection screen that a short press on the header status tag opens.
197
197
 
198
+ ## What's new in 1.0.0-beta.2
199
+
200
+ Release 1.0.0-beta.2 (2026-11-03 KST, Android `versionCode` 31) is a hotfix. The engine is unchanged (`5.1.51-zgw.3`). Full notes: [docs/releases/1.0.0-beta.2.md](docs/releases/1.0.0-beta.2.md).
201
+
202
+ - **Security check:** `zigmini scan` now works when you have not added any baseline terms (bug 268).
203
+ - **Agent rules:** common rules have fixed ids; an item in `~/.zigmini/rules.md` can replace one with `<!--replaces:<id>-->`. Check your file with `zigmini rules check`.
204
+
198
205
  ## What's new in 1.0.0-beta.1
199
206
 
200
207
  Release 1.0.0-beta.1 (2026-11-02 KST, Android `versionCode` 30) is the first candidate for 1.0.0. It selects engine `5.1.51-zgw` revision 3 (OMO 5.1.51, immutable build `5.1.51-zgw.3`, senpi 2026.10.27-1, thread schema 10). Full notes: [docs/releases/1.0.0-beta.1.md](docs/releases/1.0.0-beta.1.md).
@@ -321,6 +328,11 @@ The bridge appends two rule files after the instructions of each agent session.
321
328
  - `assets/agent/gateway-rules.md` in the package: rules common to all installations. An update changes it.
322
329
  - `~/.zigmini/rules.md`: rules only for this installation (for example the form of address, the language, accounts). You edit it yourself. When the two files conflict, this file wins.
323
330
 
331
+ Replacing a common rule item: each item in the common rules file has a fixed id in a comment (for example `<!--id:f2468e-->`). The id is set once when the item is made and stays the same when its text changes. The item numbers are only a reading order, not a priority. To replace a whole common item, put `<!--replaces:<id>-->` inside an item of `rules.md`. The gateway then leaves out that common item and uses your item. An item without `replaces` is a new rule and goes after the common rules, as before; `rules.md` still wins on a conflict. If an id no longer exists in the common rules (for example after an update), the item is added as a new rule and `zigmini doctor` shows a warning.
332
+
333
+ - `zigmini rules`: lists the common items with number, id and first line.
334
+ - `zigmini rules check`: shows each `rules.md` item as "replaces common N", "new rule" or "unknown id" (exit code 1 when an unknown id exists).
335
+
324
336
  Sessions that open after a change use the new content. To apply it at once, run `zigmini restart bridge` while nothing is running.
325
337
 
326
338
  The bridge also tells every session your profile nickname at each turn, so you don't need a rule for it. Main, work and agent sessions get it. Main also gets the default agent name, and each agent gets the name you gave it when you created or edited it. A changed nickname or name applies from the next turn. While the nickname is the default ("Me" or "나"), sessions are told that no nickname is set.
@@ -1,27 +1,37 @@
1
1
  <!--
2
2
  zigmini 패키지 공통 에이전트 규칙입니다. 게이트웨이 브리지가 main과 작업 세션의 지시문 뒤에 붙입니다.
3
3
  업데이트하면 이 파일도 바뀝니다. 설치마다 다른 규칙은 홈 폴더의 rules.md에 씁니다.
4
+ 각 항목의 번호는 사람이 읽기 위한 순서일 뿐이고 우선순위가 아닙니다.
5
+ 각 항목 첫 줄의 id 주석(6자리 16진수)은 그 항목의 고정 ID입니다. 항목을 만들 때 한 번 정하고, 문장을 고쳐도 바꾸지 않습니다.
6
+ 새 항목에는 겹치지 않는 새 ID를 붙이고, 지운 항목의 ID는 다시 쓰지 않습니다.
7
+ 홈 폴더의 rules.md 항목에 replaces:<ID> 주석을 적으면 게이트웨이는 그 ID의 공통 항목을 통째로 빼고 rules.md 항목만 씁니다.
8
+ 목록은 zigmini rules, 대체 상태는 zigmini rules check로 봅니다.
4
9
  이 주석은 지시문에 붙지 않습니다.
5
10
  -->
6
- 1. 사용자의 답이나 허가가 필요하면 확인함 질문(ask_user_question)으로 묻습니다. 허가 질문과 필수 질문은 required를 true로 합니다. main은 질문을 올린 뒤에도 다음 메시지를 처리합니다. main에서는 waitForAnswer가 true여도 게이트웨이가 차례를 멈추지 않고 질문을 확인함에 열어 둡니다. 답은 나중에 사용자 메시지(ZGW_DECISION_COMMAND)로 옵니다. main은 필수 질문의 답이 올 때까지 그 질문에 걸린 일만 하지 않습니다. 작업 세션의 필수 질문은 waitForAnswer도 true로 해서, 답이 올 때까지 그 작업만 멈춥니다.
7
- 2. 질문은 사용자만 답할 수 있고 결과를 바꾸는 것만 남깁니다. 규칙, 기억, 이미 아는 사실로 답할 수 있거나 안전한 기본값이 있으면 묻지 않고 그 기본값을 적습니다. 여러 항목을 물을 때는 각 항목이 따로 정하는 결정인지, 하나를 고르는 것인지 먼저 밝힙니다. 선택지마다 결과를 적습니다.
8
- 3. 필수 확인 질문을 하면 답이 올 때까지 그 질문에 걸린 일을 멈춥니다. 필수가 아닌 질문은 시간이 지나면 판단대로 진행합니다.
9
- 4. 사용자의 질문은 정보 요청입니다. 확인해서 답만 하고, 진행 중인 작업 방식을 바꾸지 않습니다. 작업 방식은 명시적인 지시가 있을 때만 바꿉니다.
10
- 5. 기억이나 예정 메모만 보고 작업을 시작하지 않습니다. "이야기하고 있었다"는 대화를 이어 가라는 뜻입니다. 작업은 지금 대화의 명시적인 지시가 있을 때만 시작합니다.
11
- 6. 바로 답하지 못하는 차례에서는 첫 도구 호출 전에 프리코멘트를 한 번 보냅니다.
12
- 7. main의 할 일 목록에 위임한 작업의 할 일을 옮겨 적지 않습니다. 새 결과가 없는 관찰 알림(시작, 대기, 연결 상태 등)은 채팅에 올리지 않습니다. 같은 보고가 다시 오면 한 줄로만 알립니다.
13
- 8. 시험, 계약 테스트, 끝에서 끝 확인으로 띄운 게이트웨이, 브리지, OMO 자식 프로세스와 임시 폴더는 성공, 실패, 중단 모두에서 정리합니다. 코드 변경, 산출물, 검증 증거는 지우지 않습니다.
14
- 9. 웹 화면 확인에는 Bun.WebView와 WebKit을 쓰지 않습니다. 작업이 띄운 headless Chrome을 마이크·카메라 가짜 장치 옵션으로 쓰고, 끝나면 종료합니다. 사용자 브라우저를 쓸 때는 작업이 연 백그라운드 탭만 씁니다. 화면 흉내, 뷰포트 같은 설정은 작업 탭에만 걸고 탭을 닫기 전에 지웁니다.
15
- 10. 게이트웨이가 떠 있는 동안 운영 DB를 다른 프로세스로 열지 않습니다. 조회는 게이트웨이 API와 로그로 합니다. <!--repo-security-check-->저장소 보안검사는 `zigmini scan <저장소>`로 하고 종료 코드를 그대로 보고합니다(0 깨끗함, 1 발견, 2 오류, 3 기준 사용 불가, 4 설정에서 결과물 검사 꺼짐). 이 명령이 보내는 읽기 전용 `GET /api/settings/security-check/baseline/revision`은 허용됩니다. 종료 코드 3이나 4이면 기준 파일을 직접 만들거나 고치지 않고 사용자에게 알립니다. 보고에는 규칙·위치·건수만 쓰고 값은 쓰지 않습니다.<!--/repo-security-check-->
16
- 11. 보고에는 증거가 있는 것만 씁니다. 확인된 것과 추정을 나눕니다. 코드 검토, 테스트 통과, dev 머지, 운영 배포, OMO 교체, 앱 설치를 구분하고 각각의 주체와 대상을 적습니다. 다시 시작한 뒤에는 짧은 "재시작 완료" 알림을 먼저 보냅니다.
17
- 12. 로컬 파일 경로를 웹 링크처럼 보여 주지 않습니다.
18
- 13. 사용자의 시간 표현은 사용자의 현지 시간으로 해석합니다. 보고하는 시각도 현지 시간으로 씁니다.
19
- 14. 사용자는 게이트웨이 프로필의 닉네임으로 부릅니다.
20
- 15. main은 오래 기다리거나 오래 실행하는 작업(배포, 전체 테스트, 뮤테이션 테스트, 다른 작업 기다리기)을 맡기면, 그 작업의 세션 기록이 30분 넘게 바뀌지 않을 때 main을 깨우는 감시를 직접 겁니다. 작업이 끝나면 감시를 끕니다. 작업의 "기다리는 중"·"진행 중" 보고만 믿지 않습니다. 사용자에게 그렇게 말하기 전에 그 작업의 마지막 기록 시각과 실행 중인 프로세스를 확인합니다. 작업이 멈췄으면 바로 그 작업에 조치를 보내고 사용자에게 알립니다. 작업은 긴 명령을 세션과 분리해 실행하고, 결과 파일을 직접 확인하며, 시간 상한을 겁니다.
21
- 16. 작업 하나에 나눌 수 있는 항목이 여럿이거나(예: 버그 여러 개, 구현·검증·문서 단계) 앞 단계를 기다려야 하는 단계가 있으면, 작업 세션은 mass-ulw 스킬을 씁니다. 작업 세션은 처음에 단계와 하위 작업을 정합니다. 서로 기다릴 필요가 없는 하위 작업은 최대한 병렬로 돌립니다. main은 이런 작업을 맡길 때 작업 지시문에 mass-ulw를 쓰라고 적습니다. 파일 하나 고침, 문서 한 줄 같은 작은 작업에는 mass-ulw를 쓰지 않습니다. 같은 파일을 고치는 하위 작업은 같은 단계에서 함께 돌리지 않습니다. 이런 하위 작업은 각자 worktree를 쓰거나 순서대로 돌립니다. 하위 작업을 병렬로 돌려도 무거운 명령(전체 테스트, 뮤테이션 테스트, 빌드 등)은 그 프로젝트나 설치 규칙이 정한 동시 실행 상한을 지킵니다. 규칙 8의 정리와 규칙 15의 감시는 하위 작업에도 적용합니다. 사용자가 질문만 하면 진행 중인 작업을 mass-ulw로 바꾸지 않습니다(규칙 4).
22
- 17. 모든 사용자용 글(main 답변, 작업 보고, 프리코멘트, 확인함 질문)에 ASD-STE100의 명확한 기술 글쓰기 원칙을 적용합니다.
11
+ 1. <!--id:3f9a21--> 사용자의 답이나 허가가 필요하면 확인함 질문(ask_user_question)으로 묻습니다. 허가 질문과 필수 질문은 required를 true로 합니다. main은 질문을 올린 뒤에도 다음 메시지를 처리합니다. main에서는 waitForAnswer가 true여도 게이트웨이가 차례를 멈추지 않고 질문을 확인함에 열어 둡니다. 답은 나중에 사용자 메시지(ZGW_DECISION_COMMAND)로 옵니다. main은 필수 질문의 답이 올 때까지 그 질문에 걸린 일만 하지 않습니다. 작업 세션의 필수 질문은 waitForAnswer도 true로 해서, 답이 올 때까지 그 작업만 멈춥니다.
12
+ 2. <!--id:b70c5e--> 질문은 사용자만 답할 수 있고 결과를 바꾸는 것만 남깁니다. 규칙, 기억, 이미 아는 사실로 답할 수 있거나 안전한 기본값이 있으면 묻지 않고 그 기본값을 적습니다. 여러 항목을 물을 때는 각 항목이 따로 정하는 결정인지, 하나를 고르는 것인지 먼저 밝힙니다. 선택지마다 결과를 적습니다.
13
+ 3. <!--id:4d28e6--> 필수 확인 질문을 하면 답이 올 때까지 그 질문에 걸린 일을 멈춥니다. 필수가 아닌 질문은 시간이 지나면 판단대로 진행합니다.
14
+ 4. <!--id:91ae07--> 사용자의 질문은 정보 요청입니다. 확인해서 답만 하고, 진행 중인 작업 방식을 바꾸지 않습니다. 작업 방식은 명시적인 지시가 있을 때만 바꿉니다.
15
+ 5. <!--id:c5f312--> 기억이나 예정 메모만 보고 작업을 시작하지 않습니다. "이야기하고 있었다"는 대화를 이어 가라는 뜻입니다. 작업은 지금 대화의 명시적인 지시가 있을 때만 시작합니다.
16
+ 6. <!--id:6be490--> 바로 답하지 못하는 차례에서는 첫 도구 호출 전에 프리코멘트를 한 번 보냅니다. main은 도구를 쓰는 모든 차례에서 프리코멘트(zgw_precomment)를 첫 번째 도구 호출로 보냅니다. 코드·파일 확인, 기억 저장, 작업 시작·지시·게시, 작업 알림 처리 차례도 예외가 아닙니다.
17
+ 7. <!--id:e0d71b--> main의 할 일 목록에 위임한 작업의 할 일을 옮겨 적지 않습니다. 새 결과가 없는 관찰 알림(시작, 대기, 연결 상태 등)은 채팅에 올리지 않습니다. 같은 보고가 다시 오면 한 줄로만 알립니다.
18
+ 8. <!--id:2a6c9f--> 시험, 계약 테스트, 끝에서 끝 확인으로 띄운 게이트웨이, 브리지, OMO 자식 프로세스와 임시 폴더는 성공, 실패, 중단 모두에서 정리합니다. 코드 변경, 산출물, 검증 증거는 지우지 않습니다.
19
+ 9. <!--id:8f15d4--> 브라우저 작업은 다음 순서로 방식을 고릅니다. 아래 순위를 쓰면 위 순위가 안 된 이유를 보고에 적습니다. 작업 지시문에도 이 순서를 적습니다.
20
+ - 1순위: Aside 공식 CLI. 일을 맡길 때는 `aside exec`, 탭·화면·DOM을 직접 다룰 때는 `aside repl`을 씁니다. repl을 쓰기 전에 `aside guide repl` 설명을 따릅니다. `aside help`는 빈 에이전트 세션을 만들므로 실행하지 않습니다. 도움말은 `aside --help`, `aside <명령> --help`로 봅니다.
21
+ - 2순위: BrowserSkill 확장 프로그램(`bsk`, browser 스킬의 attached 엔진). 1순위로 해결되지 않을 때만 씁니다.
22
+ - 3순위: 작업이 띄운 headless Chrome. 마이크·카메라 가짜 장치 옵션으로 띄우고, 끝나면 종료합니다.
23
+ - 웹 화면 확인에는 Bun.WebView와 WebKit을 쓰지 않습니다.
24
+ - 어느 방식이든 작업이 연 백그라운드 탭만 쓰고, 끝나면 닫습니다. 사용자 탭, 쿠키, 설정, 화면 포커스는 건드리지 않습니다. 화면 흉내, 뷰포트 같은 설정은 작업 탭에만 걸고 탭을 닫기 전에 지웁니다.
25
+ 10. <!--id:d34b70--> 게이트웨이가 떠 있는 동안 운영 DB를 다른 프로세스로 열지 않습니다. 조회는 게이트웨이 API와 로그로 합니다. <!--repo-security-check-->저장소 보안검사는 `zigmini scan <저장소>`로 하고 종료 코드를 그대로 보고합니다(0 깨끗함, 1 발견, 2 오류, 3 기준 사용 불가, 4 설정에서 결과물 검사 꺼짐). 이 명령이 보내는 읽기 전용 `GET /api/settings/security-check/baseline/revision`은 허용됩니다. 종료 코드 3이나 4이면 기준 파일을 직접 만들거나 고치지 않고 사용자에게 알립니다. 보고에는 규칙·위치·건수만 쓰고 값은 쓰지 않습니다.<!--/repo-security-check-->
26
+ 11. <!--id:57e2a8--> 보고에는 증거가 있는 것만 씁니다. 확인된 것과 추정을 나눕니다. 코드 검토, 테스트 통과, dev 머지, 운영 배포, OMO 교체, 앱 설치를 구분하고 각각의 주체와 대상을 적습니다. 다시 시작한 뒤에는 짧은 "재시작 완료" 알림을 먼저 보냅니다.
27
+ 12. <!--id:a90f63--> 로컬 파일 경로를 웹 링크처럼 보여 주지 않습니다.
28
+ 13. <!--id:1c7b5d--> 사용자의 시간 표현은 사용자의 현지 시간으로 해석합니다. 보고하는 시각도 현지 시간으로 씁니다.
29
+ 14. <!--id:f2468e--> 사용자는 게이트웨이 프로필의 닉네임으로 부릅니다.
30
+ 15. <!--id:7d903c--> main은 오래 기다리거나 오래 실행하는 작업(배포, 전체 테스트, 뮤테이션 테스트, 다른 작업 기다리기)을 맡기면, 그 작업의 세션 기록이 30분 넘게 바뀌지 않을 때 main을 깨우는 감시를 직접 겁니다. 작업이 끝나면 감시를 끕니다. 작업의 "기다리는 중"·"진행 중" 보고만 믿지 않습니다. 사용자에게 그렇게 말하기 전에 그 작업의 마지막 기록 시각과 실행 중인 프로세스(살아 있는지, CPU를 계속 100% 쓰는지)를 확인합니다. 작업이 멈췄으면 바로 그 작업에 조치를 보내고 사용자에게 알립니다. 작업은 긴 명령을 세션과 분리해(nohup, setsid 등) 실행하고, 결과 파일을 직접 확인하며, 시간 상한을 겁니다.
31
+ 16. <!--id:0e5fa9--> 작업 하나에 나눌 수 있는 항목이 여럿이거나(예: 버그 여러 개, 구현·검증·문서 단계) 앞 단계를 기다려야 하는 단계가 있으면, 작업 세션은 mass-ulw 스킬을 씁니다. 작업 세션은 처음에 단계와 하위 작업을 정합니다. 서로 기다릴 필요가 없는 하위 작업은 최대한 병렬로 돌립니다. main은 이런 작업을 맡길 때 작업 지시문에 mass-ulw를 쓰라고 적습니다. 파일 하나 고침, 문서 한 줄 같은 작은 작업에는 mass-ulw를 쓰지 않습니다. 같은 파일을 고치는 하위 작업은 같은 단계에서 함께 돌리지 않습니다. 이런 하위 작업은 각자 worktree를 쓰거나 순서대로 돌립니다. 하위 작업을 병렬로 돌려도 무거운 명령(전체 테스트, 뮤테이션 테스트, 빌드 등)은 그 프로젝트나 설치 규칙이 정한 동시 실행 상한을 지킵니다. 규칙 8의 정리와 규칙 15의 감시는 하위 작업에도 적용합니다. 사용자가 질문만 하면 진행 중인 작업을 mass-ulw로 바꾸지 않습니다(규칙 4).
32
+ 17. <!--id:b8c1e4--> 모든 사용자용 글(main 답변, 작업 보고, 프리코멘트, 확인함 질문)에 ASD-STE100의 명확한 기술 글쓰기 원칙을 적용합니다.
23
33
  - 사용자가 요청하거나 설정한 언어로 씁니다. 특정 언어를 강제하거나 영어 통제 어휘를 다른 언어에 그대로 적용하지 않습니다.
24
34
  - 짧은 능동문을 씁니다. 한 문장에는 하나의 핵심 사실이나 행동을 담습니다. 수행 주체, 작업 대상, 행동을 명확히 씁니다.
25
- - 같은 대상에는 같은 명칭을 씁니다. 필요한 전문 용어와 약어는 처음에 뜻을 설명합니다. 불필요한 표현은 줄이되 대상, 조건, 결과를 이해하는 데 필요한 정보는 남깁니다.
35
+ - 같은 대상에는 같은 명칭을 씁니다. 불필요한 동의어, 전문 용어, 약어와 대상이 불명확한 대명사를 피합니다. 필요한 전문 용어와 약어는 처음에 뜻을 설명합니다. 불필요한 표현은 줄이되 대상, 조건, 결과를 이해하는 데 필요한 정보는 남깁니다.
26
36
  - 절차는 실행 순서대로 씁니다. 각 단계에는 행동 하나를 적습니다. 필요한 선행 조건은 해당 행동 앞에 적습니다.
27
- - 결과와 계획을 구분합니다. 상태 보고는 완료된 일, 미완료된 일과 차단 원인, 다음 행동을 구분해 씁니다. 확인된 사실과 추정을 나누고 근거 없는 확신을 쓰지 않습니다. "승인", "검증", "반영", "배포"만 단독으로 쓰지 말고 누가 무엇을 허가하거나 확인하거나 바꾸었는지 명시합니다.
37
+ - 결과와 계획을 구분합니다. 상태 보고는 완료된 일, 미완료된 일과 차단 원인, 다음 행동을 구분해 씁니다. 확인된 사실과 추정을 나누고 근거 없는 확신을 쓰지 않습니다. "승인", "검증", "반영", "배포", "진행"만 단독으로 쓰지 말고 누가 무엇을 허가하거나 확인하거나 바꾸었는지 명시합니다. 예: 사용자 실행 허가, 코드 검토 통과, 대상 테스트 통과, dev 머지 완료, 게이트웨이 운영 배포, OMO 프로그램 교체.
Binary file
@@ -1,5 +1,5 @@
1
1
  {
2
- "versionCode": 30,
3
- "versionName": "1.0.0-beta.1",
4
- "sha256": "656d1c9b1f53acafc36d427351f219b3a008bb8930090f06e19448633258e53e"
2
+ "versionCode": 31,
3
+ "versionName": "1.0.0-beta.2",
4
+ "sha256": "2639dd1d6e47e1a50b8a6f73f191cf27230e48cd5a3ac7bb2e40c219f6d0d6bd"
5
5
  }
package/assets/build.json CHANGED
@@ -1 +1 @@
1
- {"commit":"a651863ce49d160d13a5ebf0399ce6e1551b6dd0"}
1
+ {"commit":"b9764a41f74f2021ac73e5e3f633c9e18bd07f8e"}
package/docs/manual.ko.md CHANGED
@@ -338,6 +338,15 @@ Android 앱(APK)을 받는 QR이 보입니다. 폰 카메라로 QR을 찍어 APK
338
338
  - **기본 에이전트.** 지그는 에이전트 목록(이름, 목적, 링크)을 볼 수 있지만 에이전트와 대화하지 않습니다. 보관함의 에이전트는 목록에 나오지 않습니다.
339
339
  - **Android.** 앱은 기본 에이전트(main) 대화만 씁니다. 에이전트 대화는 지금은 웹에서만 씁니다.
340
340
 
341
+ ### 2.12 에이전트 규칙
342
+
343
+ 게이트웨이는 패키지의 공통 규칙과 이 설치의 `~/.zigmini/rules.md`를 main과 작업 세션마다 붙입니다.
344
+
345
+ 공통 규칙 항목 바꾸기: 공통 규칙 파일의 각 항목에는 주석으로 고정 ID가 있습니다(예: `<!--id:f2468e-->`). ID는 항목을 만들 때 한 번 정하고, 문장을 고쳐도 바뀌지 않습니다. 항목 번호는 읽는 순서일 뿐이고 우선순위가 아닙니다. 공통 항목 하나를 통째로 바꾸려면 `rules.md`의 항목 안에 `<!--replaces:<ID>-->`를 적습니다. 그러면 게이트웨이는 그 공통 항목을 빼고 `rules.md`의 항목을 씁니다. `replaces`가 없는 항목은 지금처럼 새 규칙으로 공통 규칙 뒤에 붙고, 부딪치면 `rules.md`가 이깁니다. 적은 ID가 공통 규칙에 없으면(예: 업데이트로 사라짐) 그 항목은 새 규칙으로 붙고 `zigmini doctor`가 경고를 보여 줍니다.
346
+
347
+ - `zigmini rules`: 공통 항목의 번호, ID, 첫 줄을 보여 줍니다.
348
+ - `zigmini rules check`: `rules.md`의 각 항목이 "공통 N번을 대체", "신규 규칙", "없는 ID" 중 무엇인지 보여 줍니다(없는 ID가 있으면 종료 코드 1).
349
+
341
350
  ## 3. 키보드 단축키
342
351
 
343
352
  | 키 | 위치(어디서) | 동작 |
package/docs/manual.md CHANGED
@@ -336,6 +336,15 @@ Terms: the default agent (main, Zig) is the conversation the gateway has from th
336
336
  - **Default agent.** Zig can list agents (name, purpose, link) but can't talk to them. Trashed agents aren't listed.
337
337
  - **Android.** The app stays on the default agent (main) conversation. Agent conversations are web only for now.
338
338
 
339
+ ### 2.12 Agent rules
340
+
341
+ The gateway adds the common rules of the package and this installation's `~/.zigmini/rules.md` to every main and work session.
342
+
343
+ Replacing a common rule item: each item in the common rules file has a fixed id in a comment (for example `<!--id:f2468e-->`). The id is set once when the item is made and stays the same when its text changes. The item numbers are only a reading order, not a priority. To replace a whole common item, put `<!--replaces:<id>-->` inside an item of `rules.md`. The gateway then leaves out that common item and uses your item. An item without `replaces` is a new rule and goes after the common rules, as before; `rules.md` still wins on a conflict. If an id no longer exists in the common rules (for example after an update), the item is added as a new rule and `zigmini doctor` shows a warning.
344
+
345
+ - `zigmini rules`: lists the common items with number, id and first line.
346
+ - `zigmini rules check`: shows each `rules.md` item as "replaces common N", "new rule" or "unknown id" (exit code 1 when an unknown id exists).
347
+
339
348
  ## 3. Keyboard shortcuts
340
349
 
341
350
  | Key | Where | What it does |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigmini",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zigmini": "bin/zigmini"
@@ -19,6 +19,9 @@ export const INSTALL_RULES_TEMPLATE = `<!--
19
19
  이 파일은 이 설치에만 쓰는 에이전트 규칙입니다. zigmini 업데이트는 이 파일을 바꾸지 않습니다.
20
20
  여기에 쓴 내용은 게이트웨이가 main과 작업 세션의 지시문 뒤에 붙입니다. 패키지 규칙과 부딪치면 이 파일이 이깁니다.
21
21
  새 세션부터 적용됩니다(새 대화, 세션 교체, 브리지 재시작, 업데이트). 이미 열린 세션은 이전 내용을 씁니다.
22
+ 공통 규칙 항목을 통째로 바꾸려면 이 파일의 항목 안에 replaces:<공통 ID>를 HTML 주석으로 적습니다(여는 표시 <!-- 바로 뒤에 replaces:ID, 그 뒤에 닫는 표시).
23
+ 공통 항목의 번호·ID·첫 줄은 zigmini rules, 이 파일의 대체 상태는 zigmini rules check로 봅니다.
24
+ ID가 없는 항목은 새 규칙으로 뒤에 붙습니다. 공통에 없는 ID를 적으면 새 규칙으로 붙고 doctor가 경고합니다.
22
25
  최대 32KB입니다. 더 크면 게이트웨이는 이 파일을 쓰지 않습니다. 이런 주석은 지시문에 붙지 않습니다.
23
26
  -->
24
27
  `;
@@ -33,32 +36,104 @@ export function packageRulesDrop(env: Env = process.env): RegExp | undefined {
33
36
  return readVoiceSetting(runtimePaths(env).repoSecurityCheckFile) ? undefined : REPO_SECURITY_SECTION;
34
37
  }
35
38
 
36
- export function readRulesFile(path: string, drop?: RegExp): { status: RulesStatus; text: string } {
37
- if (!existsSync(path)) return { status: "missing", text: "" };
38
- let raw: string;
39
+ /** A package rule item carries a fixed id comment; it is assigned once and never derived from the text, so editing the text keeps it. */
40
+ export const RULE_ID = /<!--\s*id:([0-9a-f]{6})\s*-->/;
41
+ /** An install rule item that names a package rule id replaces that whole package item. */
42
+ export const RULE_REPLACES = /<!--\s*replaces:([0-9a-f]{6})\s*-->/;
43
+ const ITEM_START = /^\d+\.\s/;
44
+
45
+ export interface RuleItem {
46
+ readonly number: string;
47
+ readonly id: string | null;
48
+ readonly replaces: string | null;
49
+ readonly raw: string;
50
+ }
51
+
52
+ /** Splits rule text into the lines before the first numbered item and the numbered items; an item runs to the next `N. ` line. */
53
+ export function splitRuleItems(raw: string): { head: string; items: RuleItem[] } {
54
+ const head: string[] = [];
55
+ const items: string[][] = [];
56
+ for (const line of raw.split("\n")) {
57
+ if (ITEM_START.test(line)) items.push([line]);
58
+ else if (items.length === 0) head.push(line);
59
+ else items[items.length - 1]!.push(line);
60
+ }
61
+ return {
62
+ head: head.join("\n"),
63
+ items: items.map((lines) => {
64
+ const text = lines.join("\n");
65
+ return { number: lines[0]!.match(/^(\d+)\./)![1]!, id: text.match(RULE_ID)?.[1] ?? null, replaces: text.match(RULE_REPLACES)?.[1] ?? null, raw: text };
66
+ }),
67
+ };
68
+ }
69
+
70
+ function readRaw(path: string): { status: RulesStatus; raw: string } {
71
+ if (!existsSync(path)) return { status: "missing", raw: "" };
39
72
  try {
40
- if (statSync(path).size > AGENT_RULES_MAX_BYTES) return { status: "too_large", text: "" };
41
- raw = readFileSync(path, "utf8");
73
+ if (statSync(path).size > AGENT_RULES_MAX_BYTES) return { status: "too_large", raw: "" };
74
+ return { status: "used", raw: readFileSync(path, "utf8") };
42
75
  } catch {
43
- return { status: "unreadable", text: "" };
76
+ return { status: "unreadable", raw: "" };
44
77
  }
78
+ }
79
+
80
+ function finish(status: RulesStatus, raw: string, drop?: RegExp): { status: RulesStatus; text: string } {
81
+ if (status !== "used") return { status, text: "" };
45
82
  const text = (drop ? raw.replace(drop, "") : raw).replace(/<!--[\s\S]*?-->/g, "").trim();
46
83
  return text === "" ? { status: "empty", text } : { status: "used", text };
47
84
  }
48
85
 
86
+ export function readRulesFile(path: string, drop?: RegExp): { status: RulesStatus; text: string } {
87
+ const read = readRaw(path);
88
+ return finish(read.status, read.raw, drop);
89
+ }
90
+
91
+ export type MappingState = "replaces" | "new" | "unknown_id";
92
+
93
+ export interface MappingEntry {
94
+ readonly number: string;
95
+ readonly replaces: string | null;
96
+ readonly state: MappingState;
97
+ /** The human number of the replaced package item, when the id is found. */
98
+ readonly target: string | null;
99
+ }
100
+
101
+ /** How each install rule item maps onto the package items: it replaces one, it is new, or it names an id the package no longer has. */
102
+ export function ruleMapping(packageRaw: string, installRaw: string): MappingEntry[] {
103
+ const byId = new Map(splitRuleItems(packageRaw).items.filter((item) => item.id !== null).map((item) => [item.id!, item.number]));
104
+ return splitRuleItems(installRaw).items.map((item) => {
105
+ if (item.replaces === null) return { number: item.number, replaces: null, state: "new", target: null };
106
+ const target = byId.get(item.replaces) ?? null;
107
+ return { number: item.number, replaces: item.replaces, state: target === null ? "unknown_id" : "replaces", target };
108
+ });
109
+ }
110
+
111
+ /** The package text without the items an install item replaces (by id). */
112
+ export function dropReplaced(packageRaw: string, replaced: ReadonlySet<string>): string {
113
+ const { head, items } = splitRuleItems(packageRaw);
114
+ return [head, ...items.filter((item) => item.id === null || !replaced.has(item.id)).map((item) => item.raw)].join("\n");
115
+ }
116
+
49
117
  /** The text to append after the role instructions, and one log line that names the files but never their content. */
50
118
  export function agentRules(env: Env = process.env, packageFile: string = PACKAGE_RULES_FILE): { text: string; log: string } {
119
+ const installPath = join(runtimeHome(env), INSTALL_RULES_FILE);
120
+ const pkg = readRaw(packageFile);
121
+ const install = readRaw(installPath);
122
+ const mapping = install.status === "used" && pkg.status === "used" ? ruleMapping(pkg.raw, install.raw) : [];
123
+ const replaced = new Set(mapping.filter((entry) => entry.state === "replaces").map((entry) => entry.replaces!));
51
124
  const sources = [
52
- { name: "package", path: packageFile, heading: PACKAGE_RULES_HEADING, drop: packageRulesDrop(env) },
53
- { name: "install", path: join(runtimeHome(env), INSTALL_RULES_FILE), heading: INSTALL_RULES_HEADING },
125
+ { name: "package", path: packageFile, heading: PACKAGE_RULES_HEADING, read: finish(pkg.status, dropReplaced(pkg.raw, replaced), packageRulesDrop(env)) },
126
+ { name: "install", path: installPath, heading: INSTALL_RULES_HEADING, read: finish(install.status, install.raw) },
54
127
  ];
55
128
  const parts: string[] = [];
56
129
  const notes: string[] = [];
57
130
  for (const source of sources) {
58
- const read = readRulesFile(source.path, "drop" in source ? source.drop : undefined);
59
- if (read.status === "used") parts.push(`${source.heading}\n\n${read.text}`);
60
- notes.push(`${source.name}=${read.status === "used" ? `used(${source.path})` : read.status}`);
131
+ if (source.read.status === "used") parts.push(`${source.heading}\n\n${source.read.text}`);
132
+ notes.push(`${source.name}=${source.read.status === "used" ? `used(${source.path})` : source.read.status}`);
61
133
  }
134
+ const unknown = mapping.filter((entry) => entry.state === "unknown_id").map((entry) => entry.replaces);
135
+ if (replaced.size > 0) notes.push(`replaced=${[...replaced].join(",")}`);
136
+ if (unknown.length > 0) notes.push(`unknown_ids=${unknown.join(",")}`);
62
137
  return { text: parts.join("\n\n"), log: `agent rules: ${notes.join(" ")}` };
63
138
  }
64
139
 
package/src/cli/doctor.ts CHANGED
@@ -10,6 +10,7 @@ import { type InstallKind } from "../runtime/install-kind.ts";
10
10
  import { readVoiceSetting } from "../voice-setting.ts";
11
11
  import { voiceLayout } from "../voice/install.ts";
12
12
  import { parseUpdateStatus, UPDATE_STATUS_PATH, type UpdateStatus } from "../update/check.ts";
13
+ import { unknownRuleIds } from "./rules.ts";
13
14
  import { accessItem, pingAccess, readAccess } from "./access.ts";
14
15
  import { agentState, bootstrap, healthUrl, type LaunchdContext, type LaunchdDeps, ownership, plistPath, serviceLabels, waitReady } from "./launchd.ts";
15
16
 
@@ -31,6 +32,8 @@ export interface ConsentView { readonly state: "valid" | "stale" | "missing" | "
31
32
 
32
33
  export interface DoctorDeps extends LaunchdDeps {
33
34
  readonly portOpen: (port: number) => Promise<boolean>;
35
+ /** Ids in rules.md that the package rules lack; defaults to reading <home>/rules.md and the package file. */
36
+ readonly unknownRuleIds?: () => string[];
34
37
  readonly probe: (url: string) => Promise<boolean>;
35
38
  readonly doctorApi: (url: string) => Promise<DoctorCounts | null>;
36
39
  /** Version folder name `omo/current` points at, or null. */
@@ -150,6 +153,10 @@ export async function collect(deps: DoctorDeps, ctx: LaunchdContext): Promise<Do
150
153
 
151
154
  if (deps.access !== undefined) items.push(await deps.access());
152
155
 
156
+ const unknownIds = (deps.unknownRuleIds ?? (() => unknownRuleIds(ctx.paths.home)))();
157
+ if (unknownIds.length > 0) items.push({ id: "rules.mapping", label: "규칙 매핑", status: "warn",
158
+ detail: `rules.md가 공통 규칙에 없는 ID를 가리킵니다(${unknownIds.join(", ")}). 신규 규칙으로 붙습니다. \`zigmini rules check\`로 확인하세요` });
159
+
153
160
  const consent = deps.consent();
154
161
  items.push({ id: "consent", label: "권한 동의", status: consent.state === "valid" ? "ok" : consent.state === "skipped" ? "off" : "bad",
155
162
  detail: consent.state === "valid" ? `안내문 버전 ${consent.version}` : consent.state === "skipped" ? "checkout 실행" : consent.state === "stale"
package/src/cli/main.ts CHANGED
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { type LaunchdContext, type LaunchdDeps, restartCommand, startCommand, statusCommand, stopCommand, systemContext, systemDeps, UPDATER_COMMAND } from "./launchd.ts";
4
4
  import { cliGate, consentCommand, consentSummary, EXIT_DECLINED, loadNotice } from "../consent/consent.ts";
5
5
  import { installKind } from "../runtime/install-kind.ts";
6
+ import { rulesCommand } from "./rules.ts";
6
7
  import { doctorCommand, systemDoctorDeps } from "./doctor.ts";
7
8
  import { setupCommand, stdinPrompt, systemSetupDeps } from "./setup.ts";
8
9
  import { pairCommand } from "./pair.ts";
@@ -107,6 +108,10 @@ if (import.meta.main) {
107
108
  const notice = () => loadNotice();
108
109
  const hint = setupHint(argv[0], ctx.paths.home, kind);
109
110
  if (hint !== null) deps.err(`${hint}\n\n`);
111
+ if (argv[0] === "rules") {
112
+ const lang = resolveLanguage(ctx.paths.home, ctx.env);
113
+ process.exit(rulesCommand(argv.slice(1), { home: ctx.paths.home, lang: lang === "en" ? "en" : "ko", out: deps.out, err: deps.err }));
114
+ }
110
115
  if (argv[0] === "consent") {
111
116
  if (argv.length !== 1) {
112
117
  deps.err(`잘못된 인자: ${argv.join(" ")}\n\n${HELP}`);
@@ -8,7 +8,7 @@ export const LANGUAGE_FILE = "language";
8
8
  export const LANGUAGE_QUESTION = "Language / 언어 [1] English [2] 한국어: ";
9
9
 
10
10
  const BASE_MESSAGES = {
11
- help: { en: "zigmini - personal AI agent gateway\n\nNew installation: run `zigmini setup` for permission consent, engine installation, configuration and startup.\n\nUsage: zigmini <command> [options]\n\nCommands:\n security import-baseline [--from <file>] [--dry-run] Import baseline terms through the running gateway; report counts only\n setup Set up the install home, engine, configuration and launchd services\n start Register and start the gateway and bridge with launchd\n stop [--uninstall] Stop services; --uninstall also removes this package's launchd plists\n restart [gateway|bridge] Restart services\n status Show launchd, health and permission consent status\n doctor [--json] [--fix] Check the installation; --fix restarts only unhealthy launchd agents\n pair [--apk] Show a one-use phone enrollment QR, or an APK download QR (--apk); valid for 10 minutes\n rotate-main [--restart] [--wait <seconds>] [--foreground]\n Replace the main conversation's engine session after its turn ends (default wait: 900s).\n Runs detached by default; results: <home>/logs/rotate-main.log.\n --restart restarts the gateway first; --foreground runs in this terminal.\n consent Show and record full-access permission consent; stop on refusal\n update [version] [--force] Install a version (default: latest), back up the DB and restart; roll back on failure.\n --force allows active turns; the gateway must be running.\n migrate [--from <old home>] [--checkout <repository>] [--accept-license] [--dry-run]\n Copy a Docker installation (~/" + LEGACY_HOME + "); keep the old directory unchanged.\n migrate-run [--from <old home>] [--checkout <repository>] [--access ... --domain ...] [--dry-run]\n Wait for idle, stop, back up, migrate, set up and check; restore the old run on failure.\n\nSetup options:\n --lang <en|ko> Language (saved in <home>/language)\n --non-interactive Do not ask setup questions\n --accept-license Accept the engine license\n --agent-dir <path> Dedicated agent directory instead of shared ~/.omo/agent\n --notion-token-file <path> Notion token file; no token disables the calendar\n --access <tailscale|lan|domain>, --domain <https://address>\n Choose external access\n --engine-only Run only the pinned engine installation step\n\nOptions:\n --version, -v Print the installed zigmini version\n --help, -h Print this help\n", ko: "zigmini - 개인 AI 에이전트 게이트웨이\n\n처음 설치했으면 먼저 `zigmini setup`을 실행하세요. setup이 권한 동의, 엔진 설치, 설정, 시작을 합니다.\n\n사용법: zigmini <명령> [옵션]\n\n명령:\n security import-baseline [--from <파일>] [--dry-run] 실행 중인 게이트웨이로 기준 용어를 가져오고 건수만 출력합니다\n setup 설치합니다: 홈 폴더, 엔진, 설정, launchd 등록과 시작\n start 게이트웨이와 브리지를 launchd에 등록하고 시작합니다\n stop 게이트웨이와 브리지를 멈춥니다\n stop --uninstall 멈추고 이 패키지의 launchd plist를 모두 지웁니다\n restart [gateway|bridge] 다시 시작합니다\n status launchd 상태, health, 권한 동의 상태를 보여 줍니다\n doctor [--json] [--fix] 전체 상태를 점검합니다(--fix: 이상 있는 launchd 에이전트만 다시 띄움)\n pair 폰 등록 QR(유효 10분, 한 번 사용)을 터미널에 그립니다. 게이트웨이가 실행 중이어야 합니다\n pair --apk 앱(APK) 내려받기 QR(유효 10분)을 터미널에 그립니다\n rotate-main [--restart] [--wait <초>] [--foreground]\n main 대화를 새 엔진 세션으로 교체합니다. main 차례가 끝나길 기다린 뒤 보냅니다(기본 900초).\n 기본은 분리 실행이고 결과는 <홈>/logs/rotate-main.log에 남깁니다\n (--restart: 먼저 게이트웨이를 멈추고 다시 켭니다. --foreground: 이 터미널에서 실행)\n consent 풀 액세스 권한 안내를 보이고 동의를 기록합니다(거부하면 멈춤)\n update [버전] [--force] 새 버전으로 업데이트합니다(비우면 latest). DB 백업 뒤 설치·재시작하고, 실패하면 자동으로 되돌립니다\n (--force: 진행 중인 턴이 있어도 합니다. 게이트웨이가 실행 중이어야 합니다)\n migrate [--from <옛 홈>] [--checkout <저장소>] [--accept-license] [--dry-run]\n Docker 설치(~/" + LEGACY_HOME + ")를 복사해 옮깁니다. 옛 폴더는 바꾸지 않습니다\n migrate-run [--from <옛 홈>] [--checkout <저장소>] [--access ... --domain ...] [--dry-run]\n 유휴 대기 → 옛 실행 정지 → 백업 → migrate → setup → 확인, 실패하면 옛 실행으로 되돌립니다\n\nsetup 옵션:\n --lang <en|ko> Language / 언어 (saved in <home>/language)\n --non-interactive 묻지 않습니다\n --accept-license 엔진 라이선스에 동의합니다\n --agent-dir <경로> 공유 ~/.omo/agent 대신 쓸 전용 에이전트 폴더\n --notion-token-file <경로> Notion 토큰 파일(없으면 일정 기능 꺼짐)\n --access <tailscale|lan|domain>, --domain <https://주소>\n 외부 접속 선택\n --engine-only 엔진 설치 단계만 실행합니다\n\n옵션:\n --version, -v 설치된 zigmini 버전을 출력합니다\n --help, -h 이 도움말을 출력합니다\n" },
11
+ help: { en: "zigmini - personal AI agent gateway\n\nNew installation: run `zigmini setup` for permission consent, engine installation, configuration and startup.\n\nUsage: zigmini <command> [options]\n\nCommands:\n security import-baseline [--from <file>] [--dry-run] Import baseline terms through the running gateway; report counts only\n setup Set up the install home, engine, configuration and launchd services\n start Register and start the gateway and bridge with launchd\n stop [--uninstall] Stop services; --uninstall also removes this package's launchd plists\n restart [gateway|bridge] Restart services\n status Show launchd, health and permission consent status\n doctor [--json] [--fix] Check the installation; --fix restarts only unhealthy launchd agents\n rules [check] List common rule items (number, id, first line); check: how rules.md items replace them\n pair [--apk] Show a one-use phone enrollment QR, or an APK download QR (--apk); valid for 10 minutes\n rotate-main [--restart] [--wait <seconds>] [--foreground]\n Replace the main conversation's engine session after its turn ends (default wait: 900s).\n Runs detached by default; results: <home>/logs/rotate-main.log.\n --restart restarts the gateway first; --foreground runs in this terminal.\n consent Show and record full-access permission consent; stop on refusal\n update [version] [--force] Install a version (default: latest), back up the DB and restart; roll back on failure.\n --force allows active turns; the gateway must be running.\n migrate [--from <old home>] [--checkout <repository>] [--accept-license] [--dry-run]\n Copy a Docker installation (~/" + LEGACY_HOME + "); keep the old directory unchanged.\n migrate-run [--from <old home>] [--checkout <repository>] [--access ... --domain ...] [--dry-run]\n Wait for idle, stop, back up, migrate, set up and check; restore the old run on failure.\n\nSetup options:\n --lang <en|ko> Language (saved in <home>/language)\n --non-interactive Do not ask setup questions\n --accept-license Accept the engine license\n --agent-dir <path> Dedicated agent directory instead of shared ~/.omo/agent\n --notion-token-file <path> Notion token file; no token disables the calendar\n --access <tailscale|lan|domain>, --domain <https://address>\n Choose external access\n --engine-only Run only the pinned engine installation step\n\nOptions:\n --version, -v Print the installed zigmini version\n --help, -h Print this help\n", ko: "zigmini - 개인 AI 에이전트 게이트웨이\n\n처음 설치했으면 먼저 `zigmini setup`을 실행하세요. setup이 권한 동의, 엔진 설치, 설정, 시작을 합니다.\n\n사용법: zigmini <명령> [옵션]\n\n명령:\n security import-baseline [--from <파일>] [--dry-run] 실행 중인 게이트웨이로 기준 용어를 가져오고 건수만 출력합니다\n setup 설치합니다: 홈 폴더, 엔진, 설정, launchd 등록과 시작\n start 게이트웨이와 브리지를 launchd에 등록하고 시작합니다\n stop 게이트웨이와 브리지를 멈춥니다\n stop --uninstall 멈추고 이 패키지의 launchd plist를 모두 지웁니다\n restart [gateway|bridge] 다시 시작합니다\n status launchd 상태, health, 권한 동의 상태를 보여 줍니다\n doctor [--json] [--fix] 전체 상태를 점검합니다(--fix: 이상 있는 launchd 에이전트만 다시 띄움)\n rules [check] 공통 규칙 항목(번호, ID, 첫 줄)을 보여 줍니다(check: rules.md 항목의 대체 상태 점검)\n pair 폰 등록 QR(유효 10분, 한 번 사용)을 터미널에 그립니다. 게이트웨이가 실행 중이어야 합니다\n pair --apk 앱(APK) 내려받기 QR(유효 10분)을 터미널에 그립니다\n rotate-main [--restart] [--wait <초>] [--foreground]\n main 대화를 새 엔진 세션으로 교체합니다. main 차례가 끝나길 기다린 뒤 보냅니다(기본 900초).\n 기본은 분리 실행이고 결과는 <홈>/logs/rotate-main.log에 남깁니다\n (--restart: 먼저 게이트웨이를 멈추고 다시 켭니다. --foreground: 이 터미널에서 실행)\n consent 풀 액세스 권한 안내를 보이고 동의를 기록합니다(거부하면 멈춤)\n update [버전] [--force] 새 버전으로 업데이트합니다(비우면 latest). DB 백업 뒤 설치·재시작하고, 실패하면 자동으로 되돌립니다\n (--force: 진행 중인 턴이 있어도 합니다. 게이트웨이가 실행 중이어야 합니다)\n migrate [--from <옛 홈>] [--checkout <저장소>] [--accept-license] [--dry-run]\n Docker 설치(~/" + LEGACY_HOME + ")를 복사해 옮깁니다. 옛 폴더는 바꾸지 않습니다\n migrate-run [--from <옛 홈>] [--checkout <저장소>] [--access ... --domain ...] [--dry-run]\n 유휴 대기 → 옛 실행 정지 → 백업 → migrate → setup → 확인, 실패하면 옛 실행으로 되돌립니다\n\nsetup 옵션:\n --lang <en|ko> Language / 언어 (saved in <home>/language)\n --non-interactive 묻지 않습니다\n --accept-license 엔진 라이선스에 동의합니다\n --agent-dir <경로> 공유 ~/.omo/agent 대신 쓸 전용 에이전트 폴더\n --notion-token-file <경로> Notion 토큰 파일(없으면 일정 기능 꺼짐)\n --access <tailscale|lan|domain>, --domain <https://주소>\n 외부 접속 선택\n --engine-only 엔진 설치 단계만 실행합니다\n\n옵션:\n --version, -v 설치된 zigmini 버전을 출력합니다\n --help, -h 이 도움말을 출력합니다\n" },
12
12
  permissionNotice: { en: "zigmini permission notice (notice version 1)\n\n1. The zigmini gateway runs the OMO agent with the full-access permission preset.\n2. The full-access preset allows every permission on every target. The agent does not ask for approval for each tool call.\n3. The agent has file read, write and edit tools, and a shell command (bash) tool.\n4. Shell commands run with this macOS account's permissions, without a sandbox or isolation.\n5. The agent uses the network for model API calls, web search (web_search) and web fetching (webfetch).\n6. The agent uses model accounts logged in through auth.json in the agent directory and consumes their usage allowance.\n7. The default agent directory is ~/.omo/agent. Other OMO instances using that directory share its login information (auth.json) and settings (settings.json).\n8. The gateway and bridge run as this macOS user's LaunchAgents. launchd restarts them after an error exit.\n9. Requests from enrolled phone apps run with the same full-access permissions.\n10. If you do not agree, zigmini installs nothing and does not start the gateway or bridge.", ko: "zigmini 권한 안내 (안내문 버전 1)\n\n1. zigmini 게이트웨이는 OMO 에이전트를 권한 프리셋 full-access로 실행합니다.\n2. full-access 프리셋은 모든 권한, 모든 대상에 '허용' 규칙 하나만 둡니다. 그래서 에이전트는 도구를 쓸 때마다 사용자 승인을 받지 않습니다.\n3. 에이전트에는 파일 읽기(read), 쓰기(write), 고치기(edit) 도구와 셸 명령(bash) 도구가 있습니다.\n4. 셸 명령은 샌드박스 없이 이 macOS 사용자 계정의 권한으로 실행됩니다. 격리는 없습니다.\n5. 에이전트는 네트워크를 씁니다: 모델 API 호출, 웹 검색(web_search), 웹 가져오기(webfetch).\n6. 에이전트는 에이전트 폴더의 auth.json에 로그인한 모델 계정을 쓰고, 그 계정의 사용량을 소비합니다.\n7. 기본 에이전트 폴더는 ~/.omo/agent입니다. 같은 폴더를 쓰는 다른 OMO와 로그인 정보(auth.json)와 설정(settings.json)을 같이 씁니다.\n8. 게이트웨이와 브리지는 이 macOS 사용자의 LaunchAgent로 실행되고, 오류로 끝나면 launchd가 다시 시작합니다.\n9. 등록한 폰 앱이 보낸 요청도 같은 full-access 권한으로 실행됩니다.\n10. 동의하지 않으면 zigmini는 아무것도 설치하지 않고, 게이트웨이와 브리지는 시작하지 않습니다." },
13
13
  unknownOption: { en: "Unknown option: {flag}", ko: "알 수 없는 옵션: {flag}" },
14
14
  missingValue: { en: "{flag} requires a value", ko: "{flag} 에 값이 필요합니다" },
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { INSTALL_RULES_FILE, PACKAGE_RULES_FILE, ruleMapping, splitRuleItems } from "../agent-rules.ts";
4
+
5
+ /**
6
+ * `zigmini rules` lists the package (common) rule items with their number, fixed id and first line.
7
+ * `zigmini rules check` shows how each item of <home>/rules.md maps onto them: it replaces one (by id), it is new, or it
8
+ * names an id the package no longer has (exit 1). The numbers are only a reading order, never a priority.
9
+ */
10
+ export interface RulesDeps {
11
+ readonly home: string;
12
+ readonly lang: "en" | "ko";
13
+ readonly out: (text: string) => void;
14
+ readonly err: (text: string) => void;
15
+ readonly packageFile?: string;
16
+ }
17
+
18
+ const TEXT = {
19
+ en: { usage: "Usage: zigmini rules [check]", noPackage: "package rules file not found", noInstall: "no rules.md in this installation: nothing to map",
20
+ replaces: (id: string, target: string) => `replaces common ${target} (${id})`, fresh: "new rule (appended after the common rules)",
21
+ unknown: (id: string) => `unknown id ${id}: appended as a new rule; fix or remove the replaces comment` },
22
+ ko: { usage: "사용법: zigmini rules [check]", noPackage: "공통 규칙 파일이 없습니다", noInstall: "이 설치에 rules.md가 없습니다. 매핑할 항목이 없습니다",
23
+ replaces: (id: string, target: string) => `공통 ${target}번(${id})을 대체`, fresh: "신규 규칙(공통 규칙 뒤에 붙음)",
24
+ unknown: (id: string) => `없는 ID ${id}: 신규로 붙습니다. replaces 주석을 고치거나 지우세요` },
25
+ } as const;
26
+
27
+ function firstLine(raw: string): string {
28
+ const line = raw.split("\n")[0]!.replace(/<!--[\s\S]*?-->/g, "").replace(/^\d+\.\s*/, "").trim();
29
+ return line.length > 60 ? `${line.slice(0, 60)}…` : line;
30
+ }
31
+
32
+ function readText(path: string): string | null {
33
+ return existsSync(path) ? readFileSync(path, "utf8") : null;
34
+ }
35
+
36
+ export function rulesCommand(argv: readonly string[], deps: RulesDeps): number {
37
+ const text = TEXT[deps.lang];
38
+ if (argv.length > 1 || (argv.length === 1 && argv[0] !== "check")) {
39
+ deps.err(`${text.usage}\n`);
40
+ return 2;
41
+ }
42
+ const pkg = readText(deps.packageFile ?? PACKAGE_RULES_FILE);
43
+ if (pkg === null) {
44
+ deps.err(`${text.noPackage}\n`);
45
+ return 2;
46
+ }
47
+ if (argv.length === 0) {
48
+ for (const item of splitRuleItems(pkg).items) deps.out(`${item.number.padStart(2)} ${item.id ?? "------"} ${firstLine(item.raw)}\n`);
49
+ return 0;
50
+ }
51
+ const install = readText(join(deps.home, INSTALL_RULES_FILE));
52
+ if (install === null) {
53
+ deps.out(`${text.noInstall}\n`);
54
+ return 0;
55
+ }
56
+ const mapping = ruleMapping(pkg, install);
57
+ for (const entry of mapping) {
58
+ const state = entry.state === "replaces" ? text.replaces(entry.replaces!, entry.target!) : entry.state === "new" ? text.fresh : text.unknown(entry.replaces!);
59
+ deps.out(`${entry.number.padStart(2)} ${state}\n`);
60
+ }
61
+ return mapping.some((entry) => entry.state === "unknown_id") ? 1 : 0;
62
+ }
63
+
64
+ /** Ids named in rules.md that the package rules no longer have (doctor warns about them). */
65
+ export function unknownRuleIds(home: string, packageFile: string = PACKAGE_RULES_FILE): string[] {
66
+ const pkg = readText(packageFile);
67
+ const install = readText(join(home, INSTALL_RULES_FILE));
68
+ if (pkg === null || install === null) return [];
69
+ return ruleMapping(pkg, install).filter((entry) => entry.state === "unknown_id").map((entry) => entry.replaces!);
70
+ }
@@ -4,6 +4,7 @@ import {
4
4
  mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync,
5
5
  } from "node:fs";
6
6
  import { securityBaselinePaths, type Env, type RuntimePaths } from "../runtime/paths.ts";
7
+ import { machineTerms } from "./terms.ts";
7
8
 
8
9
  export const SCHEMA_VERSION = 1;
9
10
 
@@ -85,7 +86,8 @@ export function parseBaseline(text: string): BaselineResult {
85
86
  }
86
87
  allow.push({ value: entry.value, scopes: entry.scopes });
87
88
  }
88
- if (input.terms.length === 0) return { ok: false, error: "empty" };
89
+ // Bug 268: the automatic account term alone is a usable baseline; reject only when no term of either kind exists.
90
+ if (input.terms.length === 0 && machineTerms({ home: input.home, user: input.user }).terms.length === 0) return { ok: false, error: "empty" };
89
91
  return { ok: true, file: {
90
92
  schemaVersion: input.schemaVersion, baselineId: input.baselineId, revision: input.revision,
91
93
  terms: input.terms, home: input.home, user: input.user, allow,