axmap-cli 1.2.2 → 1.2.4
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.md +83 -82
- package/bin/axmap.mjs +67 -2
- package/governance/vote.mjs +107 -5
- package/package.json +1 -1
- package/src/governance.mjs +23 -0
- package/.claude/settings.local.json +0 -32
package/CLAUDE.md
CHANGED
|
@@ -1,82 +1,83 @@
|
|
|
1
|
-
# axMap 작업 규칙 — 색인 한 장
|
|
2
|
-
|
|
3
|
-
이 문서는 **axMap 자체를 고칠 때** 지키는 규칙의 **입구**다. 사람도 AI 에이전트도 읽는다.
|
|
4
|
-
규칙 본문은 **[`rule/`](rule/) 폴더**에 갈래별로 나뉘어 있다. **필요할 때 그 파일을 연다.**
|
|
5
|
-
|
|
6
|
-
> 🔴 **2026-09-11 에 이렇게 바뀌었다.** 전에는 이 파일 하나에 전부(764줄) 들어 있었고,
|
|
7
|
-
> Claude Code 는 **세션을 켤 때마다 그걸 통째로 읽는다.** 한 번도 안 쓰는 규칙까지
|
|
8
|
-
> 매번 값을 치렀다. 지금은 이 색인만 항상 읽히고, 본문은 **찾아갈 때만** 읽는다.
|
|
9
|
-
> 내용은 지우지 않았다 — 자리만 옮겼다.
|
|
10
|
-
|
|
11
|
-
**이 문서가 기준이 되게 하려면 `claude` 를 이 폴더에서 켠다.** Claude Code 는 **켠 폴더**를
|
|
12
|
-
기준으로 `CLAUDE.md` 를 읽는다. 팀 저장소 폴더에서 켜고 axMap 파일만 고치면, 읽히는 것은
|
|
13
|
-
팀 규칙이지 이 문서가 아니다.
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
cd C:/Users/SSAFY/Desktop/axMap
|
|
17
|
-
claude
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
axMap 은 **AI Experience(ax) 의 지도**다 — 에이전트가 코드베이스에서 길을 찾고 서로를
|
|
21
|
-
피해 가는 경험을 만든다. 팀에서는 여행 서비스를 만드는 **도구**로 쓰인다.
|
|
22
|
-
|
|
23
|
-
> **산문에서는 `axMap`, 식별자·명령·경로·패키지 이름에서는 `axmap`.**
|
|
24
|
-
> 다만 **MCP 도구 이름만 `ax_` 접두사**를 쓴다 (`ax_claim`, `ax_status` …).
|
|
25
|
-
> 환경변수 접두사는 `AXMAP_`, 장부 브랜치는 `axmap/claims` 다.
|
|
26
|
-
|
|
27
|
-
> 🔴 **팀 규칙은 여기 없다.** axMap 은 독립 저장소이고 팀은 npm 꾸러미 `axmap-cli` 로
|
|
28
|
-
> 받아 쓴다. 팀 규칙은 그 저장소의 `CONTRIBUTING.md` 에 있다 —
|
|
29
|
-
> `https://lab.ssafy.com/s15-bigdata-dist-sub1/S15P21E201`.
|
|
30
|
-
> 상대경로(`../CLAUDE.md`)로 걸지 않는다. 폴더가 옮겨지면 조용히 거짓말이 된다.
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 시작하기 전에 세 줄
|
|
35
|
-
|
|
36
|
-
| | |
|
|
37
|
-
|---|---|
|
|
38
|
-
| **개발선** | `team-sync` 다. `main` 이 아니다 → [rule/01](rule/01-이-저장소의-함정.md) |
|
|
39
|
-
| **고치기 전에** | `node bin/axmap.mjs claim <경로> --task … --intent …` → [rule/02](rule/02-선점-작업-반납.md) |
|
|
40
|
-
| **고친 코드를 돌리려면** | `node bin/axmap.mjs …`. 그냥 `axmap` 은 **다른 판**이 돈다 → [rule/01](rule/01-이-저장소의-함정.md) |
|
|
41
|
-
| **싸게 물어볼 일** | `agy --model gemini-3.8-flash-low -p='…'`. 🔴 `-p=` 는 **등호로 붙여** 쓴다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## 🔴 하지 말 것 — 한눈에
|
|
46
|
-
|
|
47
|
-
이 표만은 항상 읽힌다. 나머지는 찾아본다.
|
|
48
|
-
|
|
49
|
-
| | 왜 |
|
|
50
|
-
|---|---|
|
|
51
|
-
| `git commit --no-verify` | 장부에 없는 변경이 생기고, `audit` 이 나중에 불변식 위반으로 잡는다 |
|
|
52
|
-
| 아무 생각 없이 `git push` | 대상이 `origin`(팀 저장소)일 수 있다. axMap 은 **`personal team-sync`** |
|
|
53
|
-
| `git merge` 로 다른 갈래 가져오기 | 공통 조상이 없어 거부당한다. `git checkout <브랜치> -- <경로>` |
|
|
54
|
-
| `git merge-base` 결과를 파이프로 잇기 | 실패해도 조용하다. 종료 코드를 따로 본다 |
|
|
55
|
-
| 테스트 **개수**를 문서에 적기 | 늘릴 때마다 낡는다. 판정은 종료 코드다 |
|
|
56
|
-
| 실측 안 한 숫자 적기 | 확실하지 않으면 **"확인 못 함"** 이라고 쓴다 |
|
|
57
|
-
| 낡은 실측 지우기 | 정정한 날짜와 함께 **둘 다** 남긴다 |
|
|
58
|
-
| 선점이 방금 풀린 자리에 바로 들어가기 | 선점은 "지금" 만 막는다. 남은 결과물은 안 막는다 |
|
|
59
|
-
| 새 의존성 추가 | 추가하기 전에 **사람에게 묻는다** |
|
|
60
|
-
| 세기·고르기를 모델에게 시키기 | 스크립트가 토큰 0으로 **더 정확히** 한다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
61
|
-
| `setx` 로 PATH 고치기 | 1024자에서 **자른다.** 잘린 줄은 경고 없이 사라진다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## 규칙 본문 — 이럴 때 이 파일을 연다
|
|
66
|
-
|
|
67
|
-
문서 안에서 **"2절"** 이라고 하면 **`rule/02-…`** 를 말한다. 번호는 그대로 남겼다.
|
|
68
|
-
|
|
69
|
-
| 파일 | 언제 여나 |
|
|
70
|
-
|---|---|
|
|
71
|
-
| [00 — 백그라운드 에이전트](rule/00-백그라운드-에이전트.md) | 일을 대화에서 직접 할지 넘길지 가를 때. 프롬프트에 무엇을 박아야 하나 |
|
|
72
|
-
| [01 — 이 저장소의 함정](rule/01-이-저장소의-함정.md) | git 이 이상하게 굴 때. remote 이름·개발선·무관한 역사·`partOf()`·어느 `axmap` 이 도나 |
|
|
73
|
-
| [02 — 선점 → 작업 → 반납](rule/02-선점-작업-반납.md) | 파일을 고치기 전. 거부당했을 때. 커밋 훅이 막을 때. 쪽지 주고받기 |
|
|
74
|
-
| [03 — 검증](rule/03-검증.md) | 끝내기 전. 무엇을 돌려야 끝난 것인가. 화면 코드를 건드렸을 때 |
|
|
75
|
-
| [04 — 코드 규칙](rule/04-코드-규칙.md) | 코드를 쓸 때. 의존성·순수 로직·fail-closed·SPEC·불변식·버전 |
|
|
76
|
-
| [05 — 코드를 쓰기 전에](rule/05-코드를-쓰기-전에.md) | 무엇을 만들지 아직 안 정했을 때. 단순함·외과적 변경·선택지로 확인받기 |
|
|
77
|
-
| [06 — 답변과 문서](rule/06-답변과-문서.md) | 사람에게 답할 때. 터미널에 전부·두 가지까지·기호로만 부르지 않기·용어 풀기 |
|
|
78
|
-
| [07 — NotebookLM 자료 폴더](rule/07-notebooklm-자료폴더.md) | *"LM 에 넣게 자료 만들어줘"* 라는 말을 들었을 때 |
|
|
79
|
-
| [08 — `agy` 로 바깥 모델 부르기](rule/08-agy-외부-모델.md) | 토큰을 아끼고 싶을 때. **부르기 전에 "모델이 정말 필요한가" 부터** |
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
# axMap 작업 규칙 — 색인 한 장
|
|
2
|
+
|
|
3
|
+
이 문서는 **axMap 자체를 고칠 때** 지키는 규칙의 **입구**다. 사람도 AI 에이전트도 읽는다.
|
|
4
|
+
규칙 본문은 **[`rule/`](rule/) 폴더**에 갈래별로 나뉘어 있다. **필요할 때 그 파일을 연다.**
|
|
5
|
+
|
|
6
|
+
> 🔴 **2026-09-11 에 이렇게 바뀌었다.** 전에는 이 파일 하나에 전부(764줄) 들어 있었고,
|
|
7
|
+
> Claude Code 는 **세션을 켤 때마다 그걸 통째로 읽는다.** 한 번도 안 쓰는 규칙까지
|
|
8
|
+
> 매번 값을 치렀다. 지금은 이 색인만 항상 읽히고, 본문은 **찾아갈 때만** 읽는다.
|
|
9
|
+
> 내용은 지우지 않았다 — 자리만 옮겼다.
|
|
10
|
+
|
|
11
|
+
**이 문서가 기준이 되게 하려면 `claude` 를 이 폴더에서 켠다.** Claude Code 는 **켠 폴더**를
|
|
12
|
+
기준으로 `CLAUDE.md` 를 읽는다. 팀 저장소 폴더에서 켜고 axMap 파일만 고치면, 읽히는 것은
|
|
13
|
+
팀 규칙이지 이 문서가 아니다.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd C:/Users/SSAFY/Desktop/axMap
|
|
17
|
+
claude
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
axMap 은 **AI Experience(ax) 의 지도**다 — 에이전트가 코드베이스에서 길을 찾고 서로를
|
|
21
|
+
피해 가는 경험을 만든다. 팀에서는 여행 서비스를 만드는 **도구**로 쓰인다.
|
|
22
|
+
|
|
23
|
+
> **산문에서는 `axMap`, 식별자·명령·경로·패키지 이름에서는 `axmap`.**
|
|
24
|
+
> 다만 **MCP 도구 이름만 `ax_` 접두사**를 쓴다 (`ax_claim`, `ax_status` …).
|
|
25
|
+
> 환경변수 접두사는 `AXMAP_`, 장부 브랜치는 `axmap/claims` 다.
|
|
26
|
+
|
|
27
|
+
> 🔴 **팀 규칙은 여기 없다.** axMap 은 독립 저장소이고 팀은 npm 꾸러미 `axmap-cli` 로
|
|
28
|
+
> 받아 쓴다. 팀 규칙은 그 저장소의 `CONTRIBUTING.md` 에 있다 —
|
|
29
|
+
> `https://lab.ssafy.com/s15-bigdata-dist-sub1/S15P21E201`.
|
|
30
|
+
> 상대경로(`../CLAUDE.md`)로 걸지 않는다. 폴더가 옮겨지면 조용히 거짓말이 된다.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 시작하기 전에 세 줄
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| **개발선** | `team-sync` 다. `main` 이 아니다 → [rule/01](rule/01-이-저장소의-함정.md) |
|
|
39
|
+
| **고치기 전에** | `node bin/axmap.mjs claim <경로> --task … --intent …` → [rule/02](rule/02-선점-작업-반납.md) |
|
|
40
|
+
| **고친 코드를 돌리려면** | `node bin/axmap.mjs …`. 그냥 `axmap` 은 **다른 판**이 돈다 → [rule/01](rule/01-이-저장소의-함정.md) |
|
|
41
|
+
| **싸게 물어볼 일** | `agy --model gemini-3.8-flash-low -p='…'`. 🔴 `-p=` 는 **등호로 붙여** 쓴다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🔴 하지 말 것 — 한눈에
|
|
46
|
+
|
|
47
|
+
이 표만은 항상 읽힌다. 나머지는 찾아본다.
|
|
48
|
+
|
|
49
|
+
| | 왜 |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `git commit --no-verify` | 장부에 없는 변경이 생기고, `audit` 이 나중에 불변식 위반으로 잡는다 |
|
|
52
|
+
| 아무 생각 없이 `git push` | 대상이 `origin`(팀 저장소)일 수 있다. axMap 은 **`personal team-sync`** |
|
|
53
|
+
| `git merge` 로 다른 갈래 가져오기 | 공통 조상이 없어 거부당한다. `git checkout <브랜치> -- <경로>` |
|
|
54
|
+
| `git merge-base` 결과를 파이프로 잇기 | 실패해도 조용하다. 종료 코드를 따로 본다 |
|
|
55
|
+
| 테스트 **개수**를 문서에 적기 | 늘릴 때마다 낡는다. 판정은 종료 코드다 |
|
|
56
|
+
| 실측 안 한 숫자 적기 | 확실하지 않으면 **"확인 못 함"** 이라고 쓴다 |
|
|
57
|
+
| 낡은 실측 지우기 | 정정한 날짜와 함께 **둘 다** 남긴다 |
|
|
58
|
+
| 선점이 방금 풀린 자리에 바로 들어가기 | 선점은 "지금" 만 막는다. 남은 결과물은 안 막는다 |
|
|
59
|
+
| 새 의존성 추가 | 추가하기 전에 **사람에게 묻는다** |
|
|
60
|
+
| 세기·고르기를 모델에게 시키기 | 스크립트가 토큰 0으로 **더 정확히** 한다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
61
|
+
| `setx` 로 PATH 고치기 | 1024자에서 **자른다.** 잘린 줄은 경고 없이 사라진다 → [rule/08](rule/08-agy-외부-모델.md) |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 규칙 본문 — 이럴 때 이 파일을 연다
|
|
66
|
+
|
|
67
|
+
문서 안에서 **"2절"** 이라고 하면 **`rule/02-…`** 를 말한다. 번호는 그대로 남겼다.
|
|
68
|
+
|
|
69
|
+
| 파일 | 언제 여나 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| [00 — 백그라운드 에이전트](rule/00-백그라운드-에이전트.md) | 일을 대화에서 직접 할지 넘길지 가를 때. 프롬프트에 무엇을 박아야 하나 |
|
|
72
|
+
| [01 — 이 저장소의 함정](rule/01-이-저장소의-함정.md) | git 이 이상하게 굴 때. remote 이름·개발선·무관한 역사·`partOf()`·어느 `axmap` 이 도나 |
|
|
73
|
+
| [02 — 선점 → 작업 → 반납](rule/02-선점-작업-반납.md) | 파일을 고치기 전. 거부당했을 때. 커밋 훅이 막을 때. 쪽지 주고받기 |
|
|
74
|
+
| [03 — 검증](rule/03-검증.md) | 끝내기 전. 무엇을 돌려야 끝난 것인가. 화면 코드를 건드렸을 때 |
|
|
75
|
+
| [04 — 코드 규칙](rule/04-코드-규칙.md) | 코드를 쓸 때. 의존성·순수 로직·fail-closed·SPEC·불변식·버전 |
|
|
76
|
+
| [05 — 코드를 쓰기 전에](rule/05-코드를-쓰기-전에.md) | 무엇을 만들지 아직 안 정했을 때. 단순함·외과적 변경·선택지로 확인받기 |
|
|
77
|
+
| [06 — 답변과 문서](rule/06-답변과-문서.md) | 사람에게 답할 때. 터미널에 전부·두 가지까지·기호로만 부르지 않기·용어 풀기 |
|
|
78
|
+
| [07 — NotebookLM 자료 폴더](rule/07-notebooklm-자료폴더.md) | *"LM 에 넣게 자료 만들어줘"* 라는 말을 들었을 때 |
|
|
79
|
+
| [08 — `agy` 로 바깥 모델 부르기](rule/08-agy-외부-모델.md) | 토큰을 아끼고 싶을 때. **부르기 전에 "모델이 정말 필요한가" 부터** |
|
|
80
|
+
| [09 — 거버넌스와 권한](rule/09-거버넌스와-권한.md) | 머지 권한·봇·장부 브랜치를 건드릴 때. **왜 사람이 아니라 봇이 누르는가** |
|
|
81
|
+
|
|
82
|
+
고칠 것이 있으면 **`rule/` 안의 그 파일**을 고친다. 여기에 본문을 되돌려 놓지 않는다 —
|
|
83
|
+
두 벌이 되는 순간 어느 쪽이 진짜인지 아무도 모르게 된다.
|
package/bin/axmap.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
noticeLine,
|
|
44
44
|
registryUrl,
|
|
45
45
|
} from '../src/update.mjs'
|
|
46
|
+
import { stateDir } from '../src/repotarget.mjs'
|
|
46
47
|
import { homeRegistrations } from '../src/mcpstate.mjs'
|
|
47
48
|
import { compareBranchFor, formatOverlap } from '../src/overlap.mjs'
|
|
48
49
|
|
|
@@ -2612,6 +2613,19 @@ function cmdRun(name) {
|
|
|
2612
2613
|
* 아래에 찍히는 한 줄을 직접 실행하는 것으로만 일어난다.
|
|
2613
2614
|
*
|
|
2614
2615
|
* 종료 코드: 0 물어봤다 (최신이든 아니든) · 1 못 물어봤다
|
|
2616
|
+
*
|
|
2617
|
+
* 🔴 **이 명령만은 `process.exit()` 로 끝내지 않는다 (2026-09-10).**
|
|
2618
|
+
*
|
|
2619
|
+
* 레지스트리에 묻는 순간 fetch 가 소켓을 연다. 답을 다 읽은 뒤에도 그 소켓은
|
|
2620
|
+
* 아직 **닫히는 중**이고, 그 상태에서 `process.exit()` 를 부르면 Windows 의
|
|
2621
|
+
* node 가 `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)` 로 스스로
|
|
2622
|
+
* 중단한다. 화면에는 "최신입니다" 가 이미 찍힌 뒤라 **사람 눈에는 정상인데
|
|
2623
|
+
* 종료 코드가 127** 이다. 판정을 종료 코드로 하는 이 저장소에서 그건 조용한
|
|
2624
|
+
* 실패고, 이 명령을 검사에 넣는 순간 언제나 빨개진다.
|
|
2625
|
+
*
|
|
2626
|
+
* 한 틱 미루는 것(`setImmediate`)으로는 안 된다 — 재봤을 때 똑같이 죽었다.
|
|
2627
|
+
* 이벤트 루프가 실제로 비워질 때까지 기다려야 한다. 그래서 `process.exitCode`
|
|
2628
|
+
* 만 세우고 돌아간다. 느려지지 않는다 (둘 다 230ms 안쪽이었다).
|
|
2615
2629
|
*/
|
|
2616
2630
|
async function cmdUpdate(flags) {
|
|
2617
2631
|
const current = selfVersion()
|
|
@@ -2643,23 +2657,74 @@ async function cmdUpdate(flags) {
|
|
|
2643
2657
|
console.error(`새 버전을 확인하지 못했습니다: ${e.message}`)
|
|
2644
2658
|
console.error('네트워크가 안 되거나, 아직 배포된 적이 없는 꾸러미일 수 있습니다.')
|
|
2645
2659
|
console.error('')
|
|
2646
|
-
process.
|
|
2660
|
+
process.exitCode = 1
|
|
2661
|
+
return
|
|
2647
2662
|
}
|
|
2648
2663
|
|
|
2649
2664
|
writeCache({ latest, current, checkedAt: Date.now(), registry })
|
|
2650
2665
|
|
|
2651
2666
|
const notice = noticeLine(current, latest)
|
|
2652
2667
|
console.log(`최신 버전: ${latest}`)
|
|
2668
|
+
|
|
2669
|
+
// 🔴 **훅이 부르는 사본도 같이 본다 (2026-09-12).**
|
|
2670
|
+
//
|
|
2671
|
+
// 판이 이 PC 에 **두 군데** 있다. `axmap` 명령(전역 설치)과 git 훅이 부르는
|
|
2672
|
+
// 사본(`~/.axmap/app`)이다. `npm i -g` 는 앞의 것만 올리고 뒤의 것은 `setup`
|
|
2673
|
+
// 이 올린다 — 서로 안 따라간다.
|
|
2674
|
+
//
|
|
2675
|
+
// 그런데 이 명령은 **자기 판만 보고** "최신입니다" 라고 답했다. 훅이 부르는
|
|
2676
|
+
// 사본이 옛 판인데도 그랬다. 막는 것보다 나쁘다 — **괜찮다고 말하고 넘어간다.**
|
|
2677
|
+
// 실측 (2026-09-12): `npm i -g` 로 전역이 1.2.3 이 된 직후 이 명령이
|
|
2678
|
+
// "최신입니다" 라고 했는데 훅이 부르는 사본은 1.2.2 였다.
|
|
2679
|
+
const installed = installedAppVersion()
|
|
2680
|
+
const skewed = Boolean(installed && current && installed !== current)
|
|
2681
|
+
if (skewed) {
|
|
2682
|
+
console.log('')
|
|
2683
|
+
console.log(`🔴 git 훅이 부르는 사본은 아직 ${installed} 입니다 (이 명령은 ${current}).`)
|
|
2684
|
+
console.log(' 둘은 따로 움직입니다 — 전역은 `npm i -g`, 사본은 `axmap setup` 이 올립니다.')
|
|
2685
|
+
console.log('')
|
|
2686
|
+
console.log(' 맞추려면: axmap setup')
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2653
2689
|
console.log('')
|
|
2654
2690
|
if (notice) {
|
|
2655
2691
|
for (const l of notice.split('\n')) console.log(l)
|
|
2656
2692
|
console.log('')
|
|
2657
2693
|
console.log(' 🔴 저절로 바뀌지 않습니다. 위 한 줄을 직접 실행할 때만 바뀝니다.')
|
|
2694
|
+
} else if (skewed) {
|
|
2695
|
+
// 여기서 "최신입니다" 라고 말하면 거짓이 된다. 이 PC 의 실력은 **가장 낡은 것**이다.
|
|
2696
|
+
console.log('레지스트리 기준으로는 최신이지만, 위 사본이 아직 안 맞습니다.')
|
|
2658
2697
|
} else {
|
|
2659
2698
|
console.log('최신입니다.')
|
|
2660
2699
|
}
|
|
2661
2700
|
console.log('')
|
|
2662
|
-
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
/**
|
|
2704
|
+
* git 훅이 실제로 부르는 사본(`~/.axmap/app`)의 판. 없거나 못 읽으면 `null`.
|
|
2705
|
+
*
|
|
2706
|
+
* 🔴 **`selfVersion()` 과 다른 것을 본다.** `selfVersion()` 은 *"지금 이 코드"* 의
|
|
2707
|
+
* 판이고, 이 함수는 *"커밋할 때마다 실제로 도는 코드"* 의 판이다. 둘이 갈릴 수
|
|
2708
|
+
* 있다는 것이 이 PC 구조의 사실이므로, 하나만 보고 답하면 거짓말이 된다.
|
|
2709
|
+
*
|
|
2710
|
+
* 🔴 자리를 여기서 새로 정하지 않는다 — `stateDir()` 을 쓴다. 그래야
|
|
2711
|
+
* `AXMAP_STATE_DIR` 이 여기에도 걸린다. 그 환경변수가 있는 이유가
|
|
2712
|
+
* `src/repotarget.mjs` 에 적혀 있다 — *"테스트가 사람의 작업 환경을 고치는
|
|
2713
|
+
* 것은 어떤 이유로도 정당하지 않다."* `os.homedir()` 를 직접 부르면 이 명령의
|
|
2714
|
+
* 출력이 **돌리는 사람의 PC 상태에 따라 달라지고**, 그러면 시험이 사람마다
|
|
2715
|
+
* 다른 답을 낸다 (실제로 그렇게 깨졌다).
|
|
2716
|
+
*
|
|
2717
|
+
* 🔴 못 읽으면 `null` 을 돌려 **아무 말도 안 한다.** 사본을 안 쓰는 설치도 있고
|
|
2718
|
+
* (팀 저장소 안의 벤더링 사본은 위에서 이미 걸러진다), 없는 것을 낡았다고
|
|
2719
|
+
* 말하면 그 경고는 곧 아무도 안 읽는 잡음이 된다.
|
|
2720
|
+
*/
|
|
2721
|
+
function installedAppVersion() {
|
|
2722
|
+
try {
|
|
2723
|
+
const p = path.join(stateDir(), 'app', 'package.json')
|
|
2724
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'))?.version ?? null
|
|
2725
|
+
} catch {
|
|
2726
|
+
return null
|
|
2727
|
+
}
|
|
2663
2728
|
}
|
|
2664
2729
|
|
|
2665
2730
|
/**
|
package/governance/vote.mjs
CHANGED
|
@@ -71,7 +71,7 @@ import path from 'node:path'
|
|
|
71
71
|
import { fileURLToPath } from 'node:url'
|
|
72
72
|
import { agentNameError } from '../src/protocol.mjs'
|
|
73
73
|
import {
|
|
74
|
-
EXIT, DEFAULT_POLICY_PATH, voteNoteRule, voteNoteProblem,
|
|
74
|
+
EXIT, DEFAULT_POLICY_PATH, voteNoteRule, voteNoteProblem, selfVoteMode, DEFAULT_SELF_VOTE,
|
|
75
75
|
} from '../src/governance.mjs'
|
|
76
76
|
|
|
77
77
|
const VOTES_BRANCH = 'axmap/votes'
|
|
@@ -350,6 +350,35 @@ function votesHead() {
|
|
|
350
350
|
* 최근 200 커밋으로 떨어지고, **어느 범위를 봤는지 경고에 함께 적는다** —
|
|
351
351
|
* 근거를 안 적으면 사람이 경고를 믿을지 말지 판단할 수 없다.
|
|
352
352
|
*/
|
|
353
|
+
/**
|
|
354
|
+
* 이번 판정의 `self_vote` 모드. **세는 쪽과 같은 함수로 읽는다.**
|
|
355
|
+
*
|
|
356
|
+
* 🔴 이 함수가 없어서 버그가 났다 (1.2.3 에서 고침). 세는 쪽
|
|
357
|
+
* (`src/governance.mjs` 의 `excludedAuthors`)은 `selfVoteMode(policy)` 를 읽어
|
|
358
|
+
* `"tip"` 이면 **맨 위 커밋 author 한 명만** 빼는데, 쓰는 쪽인 이 파일은 정책을
|
|
359
|
+
* 아예 안 보고 **언제나 모든 author** 를 막았다. 그래서 **세어질 표를 던지기도
|
|
360
|
+
* 전에 exit 으로 막았다.**
|
|
361
|
+
*
|
|
362
|
+
* 실측 (팀 저장소 S15P21E201, `common/dev → main`, 2026-09-12): 정책이
|
|
363
|
+
* `"tip"` 이라 던질 수 있는 사람이 5명인데, 이 함수가 없어서 2명으로 보였다.
|
|
364
|
+
* 정족수가 2표라 **그 2명이 둘 다 던져야만** 통과하는 상태였다.
|
|
365
|
+
*
|
|
366
|
+
* 더 아픈 것은 `policy.json` 이 **정확히 그 교착을 피하려고** `"tip"` 으로
|
|
367
|
+
* 바꿔 두었다는 점이다 (2026-09-09). 정책은 고쳤는데 도구가 그 정책을 안 읽어서,
|
|
368
|
+
* 고친 줄 알고 지나갈 뻔했다. 바로 아래 `g1WaiverFor` 의 주석이 경고한
|
|
369
|
+
* *"같은 규칙을 두 곳에 적으면 그 어긋남이 그대로 다시 난다"* 가 한 번 더 난 것이다.
|
|
370
|
+
*
|
|
371
|
+
* 🔴 **fail-closed.** 못 읽거나 값이 이상하면 가장 엄한 `authors` 로 떨어진다.
|
|
372
|
+
* 모를 때 풀어 주면 G1 은 성질이 아니라 스위치가 된다.
|
|
373
|
+
*/
|
|
374
|
+
function selfVoteModeSafe(policy) {
|
|
375
|
+
try {
|
|
376
|
+
return selfVoteMode(policy)
|
|
377
|
+
} catch {
|
|
378
|
+
return DEFAULT_SELF_VOTE
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
353
382
|
function selfVoteRange(branch, targetName) {
|
|
354
383
|
const t = git(['rev-parse', '--verify', '--quiet', `${targetName}^{commit}`])
|
|
355
384
|
if (t.code === 0 && t.out) {
|
|
@@ -390,8 +419,63 @@ function g1WaiverFor(branch, targetName, policyFlag) {
|
|
|
390
419
|
return { eligibleCount, threshold }
|
|
391
420
|
}
|
|
392
421
|
|
|
393
|
-
|
|
394
|
-
|
|
422
|
+
/**
|
|
423
|
+
* **판정에 물어본다 — "이 사람이 G1 으로 빠지나?"**
|
|
424
|
+
*
|
|
425
|
+
* 🔴 이것이 1.2.4 의 요점이다. 1.2.3 까지는 쓰는 쪽이 같은 규칙을 **자기가
|
|
426
|
+
* 계산**했고, 그 어긋남이 **두 번** 났다 — 2026-09-07 의 G1 해제(세는 쪽만
|
|
427
|
+
* 알았다), 2026-09-12 의 `self_vote` 모드(쓰는 쪽이 정책을 안 읽었다).
|
|
428
|
+
* 같은 규칙을 두 곳에 적으면 몇 번을 고쳐도 또 난다. **판정자는 하나여야 한다.**
|
|
429
|
+
*
|
|
430
|
+
* 🔴 **`null` 은 "모른다" 다 — 통과도 거부도 아니다.** 게이트를 못 돌린 경우
|
|
431
|
+
* (저장소가 아직 그럴 상태가 아니거나, 정책이 없거나, 타임아웃)에는 부르는
|
|
432
|
+
* 쪽이 직접 보는 길로 떨어진다. 여기서 `false` 를 돌려 조용히 통과시키면,
|
|
433
|
+
* 게이트가 잠깐 안 도는 것만으로 G1 이 꺼진다.
|
|
434
|
+
*
|
|
435
|
+
* 🔴 **해제와 섞지 않는다.** 이 함수가 답하는 것은 *"자기 표인가"* 하나다.
|
|
436
|
+
* *"자기 표인데 그래도 세는가"* 는 `g1WaiverFor` 가 따로 답한다. 둘을 합치면
|
|
437
|
+
* 부르는 쪽이 **"자기 표가 아니다" 와 "자기 표인데 해제됐다" 를 구별 못 하고**,
|
|
438
|
+
* 그러면 해제로 통과한 사람에게 왜 그냥 써 줬는지 말해 줄 수 없다.
|
|
439
|
+
*
|
|
440
|
+
* @returns {{self:boolean, mode:string|null}|null} 못 물어봤으면 `null`
|
|
441
|
+
*/
|
|
442
|
+
function selfVoteByGate(branch, targetName, email, policyFlag) {
|
|
443
|
+
const v = runGate(branch, targetName, policyFlag)
|
|
444
|
+
if (!v || v.stage !== 'count' || !v.tally) return null
|
|
445
|
+
const list = v.tally.selfVoteEmails
|
|
446
|
+
// 옛 게이트(1.2.3 이하)는 이 칸을 안 낸다. 없는 것을 "자기 표가 아니다" 로
|
|
447
|
+
// 읽으면 판이 섞인 팀에서 G1 이 조용히 꺼진다 — 모른다고 답한다.
|
|
448
|
+
if (!Array.isArray(list)) return null
|
|
449
|
+
return {
|
|
450
|
+
self: list.includes(String(email ?? '').trim().toLowerCase()),
|
|
451
|
+
mode: typeof v.tally.selfVoteMode === 'string' ? v.tally.selfVoteMode : null,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function warnIfSelfVote(branch, targetName, email, force, policyFlag, policy) {
|
|
456
|
+
// ── 1. 먼저 판정에 물어본다 ──────────────────────────────────────────────
|
|
457
|
+
const asked = selfVoteByGate(branch, targetName, email, policyFlag)
|
|
458
|
+
if (asked && !asked.self) return // 판정이 "자기 표가 아니다" 고 했다. 끝.
|
|
459
|
+
if (asked && asked.self) {
|
|
460
|
+
return reportSelfVote(branch, targetName, email, force, policyFlag, {
|
|
461
|
+
label: `${targetName} 기준 판정`,
|
|
462
|
+
mode: asked.mode,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── 2. 못 물어봤으면 직접 본다 ───────────────────────────────────────────
|
|
467
|
+
// 🔴 **세는 쪽과 같은 함수로** 정책을 읽는다. 여기서 조건을 다시 적으면 어긋난다.
|
|
468
|
+
const mode = selfVoteModeSafe(policy)
|
|
469
|
+
|
|
470
|
+
// `off` 는 G1 자체를 끈 것이다. 막을 것도 경고할 것도 없다.
|
|
471
|
+
if (mode === 'off') return
|
|
472
|
+
|
|
473
|
+
// `tip` 은 **맨 위 커밋 하나만** 본다. 오래 쌓이는 <파트>/dev 에서 옛 커밋 때문에
|
|
474
|
+
// 막히지 않게 하려고 정책이 고른 값이고, 세는 쪽이 정확히 그렇게 센다.
|
|
475
|
+
const range = mode === 'tip'
|
|
476
|
+
? { args: ['-n', '1', branch], label: `${branch} 의 맨 위 커밋` }
|
|
477
|
+
: selfVoteRange(branch, targetName)
|
|
478
|
+
|
|
395
479
|
const r = git(['log', '--format=%aE', ...range.args])
|
|
396
480
|
if (r.code !== 0) {
|
|
397
481
|
console.error(`경고: 소스 브랜치의 author 를 읽지 못해 자기 표인지 확인하지 못했습니다.\n ${r.err.split('\n')[0]}`)
|
|
@@ -400,6 +484,21 @@ function warnIfSelfVote(branch, targetName, email, force, policyFlag) {
|
|
|
400
484
|
const authors = new Set(r.out.split('\n').map((s) => s.trim().toLowerCase()).filter(Boolean))
|
|
401
485
|
if (!authors.has(email.toLowerCase())) return
|
|
402
486
|
|
|
487
|
+
reportSelfVote(branch, targetName, email, force, policyFlag, { label: range.label, mode })
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* **자기 표인 것이 확인된 뒤**, 그것을 사람에게 어떻게 말하고 막을지.
|
|
492
|
+
*
|
|
493
|
+
* 🔴 두 길(판정에 물어본 길·직접 본 길)이 **같은 코드로 끝나야 한다.** 여기를
|
|
494
|
+
* 두 벌로 두면 "판정이 답한 날" 과 "게이트를 못 돌린 날" 의 안내가 갈린다 —
|
|
495
|
+
* 그 갈라짐이 이 파일에서 이미 두 번 사고를 냈다.
|
|
496
|
+
*
|
|
497
|
+
* @param {{label:string, mode:string}|null} seen 직접 본 길만 채운다. 판정에
|
|
498
|
+
* 물어본 길에서는 `null` — 그때는 무엇을 봤는지 이 파일이 모르고,
|
|
499
|
+
* **모르는 것을 지어내 적지 않는다.**
|
|
500
|
+
*/
|
|
501
|
+
function reportSelfVote(branch, targetName, email, force, policyFlag, seen) {
|
|
403
502
|
// 자기 표는 맞다. 그런데 **이 판정에서는 세어지는가?** 세는 쪽에 물어본다.
|
|
404
503
|
const waiver = g1WaiverFor(branch, targetName, policyFlag)
|
|
405
504
|
if (waiver) {
|
|
@@ -414,7 +513,10 @@ function warnIfSelfVote(branch, targetName, email, force, policyFlag) {
|
|
|
414
513
|
|
|
415
514
|
console.error(
|
|
416
515
|
`이 표는 자기 표라 세지지 않습니다 (G1 — 자기가 쓴 코드에 자기가 찬성할 수 없습니다).\n`
|
|
417
|
-
+
|
|
516
|
+
+ (seen
|
|
517
|
+
? ` ${email} 이(가) ${seen.label} 의 author 목록에 있습니다.\n`
|
|
518
|
+
+ ` 배제 범위는 정책의 self_vote = "${seen.mode}" 가 정합니다.\n\n`
|
|
519
|
+
: ` ${email} 을(를) ${targetName} 기준 판정이 자기 표로 셉니다.\n\n`)
|
|
418
520
|
+ ' 그래도 남기려면 --force 를 붙이세요. 파일은 남고 판정문에는 "안 센 표" 로 보입니다.\n'
|
|
419
521
|
+ ' 조용히 무효표를 쌓지 않는 이유: 던졌다고 믿는 사람이 아무도 안 세는 표를 기다립니다.',
|
|
420
522
|
)
|
|
@@ -718,7 +820,7 @@ const nameErr = agentNameError(voter)
|
|
|
718
820
|
if (nameErr) die(`${nameErr}\n git config user.name 을 고치세요.`)
|
|
719
821
|
|
|
720
822
|
warnIfNotAVoter(roster, targetName, email)
|
|
721
|
-
warnIfSelfVote(branch, targetName, email, flags.force === true, str(flags.policy))
|
|
823
|
+
warnIfSelfVote(branch, targetName, email, flags.force === true, str(flags.policy), policy)
|
|
722
824
|
|
|
723
825
|
// ── 이유 없는 표는 여기서 거부한다 ─────────────────────────────────────────
|
|
724
826
|
//
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//name": "🔴 npm 의 'axmap' 은 2022년부터 남이 쓰고 있는 다른 꾸러미다(leozin_ 의 Map 라이브러리, 1.1.2). 그래서 `npx axmap` 은 우리 것이 아니고, 누구에게도 그렇게 안내하면 안 된다. 2026-08-28 에 axmap-cli 로 정했다 — 스코프(@사용자/이름)를 쓰지 않는 이유는 스코프가 npm 계정 이름에 묶여서 나중에 팀이나 조직으로 옮길 때 이름이 통째로 바뀌기 때문이다. 사람이 치는 **명령** 이름은 그대로 axmap 이다 (아래 bin) — 꾸러미 이름과 명령 이름은 달라도 된다.",
|
|
3
3
|
"name": "axmap-cli",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.4",
|
|
5
5
|
"//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
|
|
6
6
|
"//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
|
|
7
7
|
"publishConfig": {
|
package/src/governance.mjs
CHANGED
|
@@ -1064,6 +1064,29 @@ export function countVotes({
|
|
|
1064
1064
|
eligibleCount,
|
|
1065
1065
|
threshold: need,
|
|
1066
1066
|
selfVoted,
|
|
1067
|
+
// ── 🔴 G1 이 배제하는 email 들 (2026-09-12) ──────────────────────────
|
|
1068
|
+
//
|
|
1069
|
+
// 쓰는 쪽(`governance/vote.mjs`)이 *"내 표가 세어지나"* 를 **여기에 물어볼 수
|
|
1070
|
+
// 있게** 한다. 그러지 않으면 쓰는 쪽이 같은 규칙을 자기가 계산해야 하고,
|
|
1071
|
+
// 그 어긋남이 실제로 **두 번** 났다 — 2026-09-07 의 G1 해제, 2026-09-12 의
|
|
1072
|
+
// `self_vote` 모드. 판정자는 하나여야 한다.
|
|
1073
|
+
//
|
|
1074
|
+
// 🔴 **명단으로 거르지 않는다.** 여기 담기는 것은 `excludedAuthors` 가 낸
|
|
1075
|
+
// 집합 그대로다. 명단으로 좁히면 명단 밖 사람에게는 경고가 안 가고, 그
|
|
1076
|
+
// 사람은 **아무도 안 세는 표를 던져 놓고 기다리게 된다** — 이 저장소가
|
|
1077
|
+
// 자기 표를 조용히 쌓지 않기로 한 이유가 그것이다.
|
|
1078
|
+
// (명단에 있나 없나는 `warnIfNotAVoter` 가 따로, 막지 않고 알린다.)
|
|
1079
|
+
//
|
|
1080
|
+
// 🔴 **해제(`g1Waived`)가 걸려도 비우지 않는다.** 이 칸은 *"이 판정에서
|
|
1081
|
+
// 누가 자기 표인가"* 이고, 해제는 *"그 자기 표를 그래도 세는가"* 다.
|
|
1082
|
+
// 둘은 다른 사실이라 따로 내야 한다 — 합쳐서 비워 버리면 읽는 쪽이
|
|
1083
|
+
// "자기 표가 아니다" 와 "자기 표인데 해제됐다" 를 **구별할 수 없고**,
|
|
1084
|
+
// 그러면 해제로 통과한 사람에게 *왜* 그냥 써 줬는지 말해 줄 수 없다.
|
|
1085
|
+
// (읽는 쪽은 이 칸과 `g1Waived` 를 같이 본다.)
|
|
1086
|
+
selfVoteEmails: [...authors],
|
|
1087
|
+
// 어느 규칙으로 위 집합을 만들었는가. 막힌 사람에게 **왜** 막혔는지 말해 주려면
|
|
1088
|
+
// 읽는 쪽이 이 값을 알아야 한다 — 모르면 "자기 표라서" 까지밖에 못 말한다.
|
|
1089
|
+
selfVoteMode: selfVote,
|
|
1067
1090
|
}
|
|
1068
1091
|
}
|
|
1069
1092
|
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(sed -i 's|/\\\\*\\\\* 궤도 모드에서 허용하는 yaw 한계 \\(약 35°\\). cos 이 0.82 아래로 내려가지 않는다. \\\\*/\\\\nconst YAW_LIMIT = 0.62|X|' app/web/graph.js)",
|
|
5
|
-
"Bash(sed -i 's|const YAW_LIMIT = 0.62|const TILT_LIMIT = 0.62|; s|허용하는 yaw 한계|허용하는 기울기 한계 \\(yaw · pitch 공통\\)|' app/web/graph.js)",
|
|
6
|
-
"Bash(sed -i 's|인코딩은 인코딩이 아니다. cos\\(TILT_LIMIT\\)=0.82 이면 고리의 화면 반지름이\\\\n \\\\* \\\\[0.82R, R\\\\] 안에 머물러 이웃 단계와 절대 겹치지 않는다.|X|' app/web/graph.js)",
|
|
7
|
-
"Bash(sed -n '/인코딩은 인코딩이 아니다/,+2p' app/web/graph.js)",
|
|
8
|
-
"Bash(sed -i 's|import { applyEdits, draft, editRecord } from|import { applyEdits, draft } from|' app/server.mjs)",
|
|
9
|
-
"Bash(sed -i 's|\"test\": \"node --test test/protocol.test.mjs test/model.test.mjs test/analyze.test.mjs\"|\"test\": \"node --test test/protocol.test.mjs test/model.test.mjs test/analyze.test.mjs test/features.test.mjs\"|' package.json)",
|
|
10
|
-
"Bash(npm test *)",
|
|
11
|
-
"Bash(sed -i 's| .filter\\(\\(\\\\[t\\\\]\\) => t !== name \\\\&\\\\& !taken.has\\(t\\)\\)| .filter\\(\\([t]\\) => t !== name \\\\&\\\\& !taken.has\\(t\\) \\\\&\\\\& !tooCommon\\(t\\)\\)|' app/lib/features.mjs)",
|
|
12
|
-
"Bash(node -e ' *)",
|
|
13
|
-
"Bash(npm run *)",
|
|
14
|
-
"Bash(export AXMAP_AGENT=claude-fix GIT_AUTHOR_NAME=claude-fix GIT_AUTHOR_EMAIL=claude-fix@local GIT_COMMITTER_NAME=claude-fix GIT_COMMITTER_EMAIL=claude-fix@local)",
|
|
15
|
-
"Bash(node bin/axmap.mjs claim app/web/app.js app/web/graph.js app/web/style.css --task fix-click-color --intent \"이름 클릭=강조/더블클릭=이름변경, 단계 없을 때 색 회귀 수정\" --actor agent)",
|
|
16
|
-
"Bash(sed 's/\\\\$$//')",
|
|
17
|
-
"Bash(python -)",
|
|
18
|
-
"Bash(sed -n '/import 를 뽑을 수 있는 언어 이름/,/^\\\\]\\)/p' app/lib/analyze.mjs)",
|
|
19
|
-
"Bash(export AXMAP_AGENT=janghyojoon)",
|
|
20
|
-
"Bash(node bin/axmap.mjs release)",
|
|
21
|
-
"Bash(echo \"// 테스트\")",
|
|
22
|
-
"Bash(git add *)",
|
|
23
|
-
"Bash(git commit *)",
|
|
24
|
-
"Bash(git reset *)",
|
|
25
|
-
"Bash(git checkout *)"
|
|
26
|
-
]
|
|
27
|
-
},
|
|
28
|
-
"enabledMcpjsonServers": [
|
|
29
|
-
"axmap"
|
|
30
|
-
],
|
|
31
|
-
"enableAllProjectMcpServers": true
|
|
32
|
-
}
|