axmap-cli 1.0.0 → 1.0.1

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/bin/axmap.mjs CHANGED
@@ -1172,6 +1172,32 @@ function busSetupFailed(r) {
1172
1172
  return 'failed'
1173
1173
  }
1174
1174
 
1175
+ /**
1176
+ * 쪽지함만 준비한다. **장부는 건드리지 않는다.**
1177
+ *
1178
+ * 🔴 왜 `init` 을 부르지 않고 이 명령이 따로 있는가. 쪽지를 **읽는** 길에서
1179
+ * 부를 자리가 필요해서다. `init` 은 장부(`axmap/claims`)까지 만들고 그것을
1180
+ * 원격에 push 한다 — "내 쪽지 좀 보자" 가 남의 저장소에 **선점 장부**를
1181
+ * 만드는 일이 되면 안 된다. 쪽지 브랜치는 쪽지함이 원래 사는 곳이므로
1182
+ * 거기까지는 간다. 선을 긋는 자리는 "읽으려는 그것" 과 "그 밖의 것" 사이다.
1183
+ *
1184
+ * 그래서 `tools/bus.mjs` 는 쪽지함이 없을 때 이 명령을 부른다. 만드는 코드를
1185
+ * 저쪽에 복사하지 않는 이유는 사본이 곧 두 벌이고, 두 벌은 반드시 어긋나기
1186
+ * 때문이다 — 이 저장소가 팀 사본을 걷어낸 것과 같은 이유다.
1187
+ */
1188
+ function cmdBusRepair() {
1189
+ const root = repoRoot()
1190
+ // .axmap 을 손으로 지웠어도 git 쪽에 worktree 등록이 남아 add 가 실패한다.
1191
+ git(['worktree', 'prune'], { cwd: root })
1192
+ const r = ensureBus(root)
1193
+ if (r === 'failed') process.exit(1)
1194
+ console.log(
1195
+ r === 'ready'
1196
+ ? `쪽지함이 이미 있습니다: ${BUS_REL}`
1197
+ : `쪽지함을 만들었습니다: ${BUS_REL} (${BUS_BRANCH})`,
1198
+ )
1199
+ }
1200
+
1175
1201
  function cmdInit(flags = {}) {
1176
1202
  const root = repoRoot()
1177
1203
  const dir = ledgerDir(root)
@@ -1664,7 +1690,16 @@ function collectCodeCommits(root, range) {
1664
1690
  for (const line of log.out ? log.out.split(NL) : []) {
1665
1691
  if (!line) continue
1666
1692
  const [sha, author, ct, subject] = line.split(NUL)
1667
- const files = git(['show', '--no-renames', '--name-only', '--format=', sha], { cwd: root }).out
1693
+ // 🔴 `-c core.quotepath=false` 없으면 **한글 경로가 claim 맞는다.**
1694
+ // git 은 기본으로 ASCII 밖 글자를 역슬래시 + 8진수 세 자리로 감싸 내보낸다.
1695
+ // 그 문자열은 장부에 적힌 경로와 절대 같아질 수 없고, 검사는 "안 겹친다" 로
1696
+ // **조용히 통과**한다 - 락에서 최악인 fail-open 이다.
1697
+ // 팀 CI 가 잡 안에서 `git config core.quotepath false` 로 막고 있었다.
1698
+ // 우회는 그 잡에만 걸린다. 부르는 자리에서 못을 박는다 (verify 와 같은 방식).
1699
+ const files = git(
1700
+ ['-c', 'core.quotepath=false', 'show', '--no-renames', '--name-only', '--format=', sha],
1701
+ { cwd: root },
1702
+ ).out
1668
1703
  out.push({
1669
1704
  sha,
1670
1705
  author,
@@ -2186,6 +2221,8 @@ const HELP = `axmap - AI 에이전트 작업 선점 프로토콜
2186
2221
  axmap setup 이 PC 와 이 저장소에 axMap 을 붙인다 (첫 한 번)
2187
2222
  [--remote <이름|git 주소>] [--name] [--dry-run]
2188
2223
  axmap init 장부(고아 브랜치 + worktree)를 준비한다
2224
+ axmap bus-repair 쪽지함만 준비한다 (장부는 안 건드린다)
2225
+ 쪽지를 읽다가 "쪽지함을 못 열었습니다" 를 봤을 때
2189
2226
  axmap claim <경로...> 경로를 선점한다 [--task --intent --ttl 30m]
2190
2227
  axmap release [경로...] 반납한다 (경로 생략 시 전부)
2191
2228
  axmap renew TTL 을 연장한다 [--ttl 30m]
@@ -2197,6 +2234,7 @@ const HELP = `axmap - AI 에이전트 작업 선점 프로토콜
2197
2234
  axmap doctor 이 PC 에서 선점이 실제로 도는지 점검 [--json]
2198
2235
  axmap hook install verify 를 pre-commit 훅으로 설치
2199
2236
  axmap update 새 버전이 있는지 묻는다 (바꾸지는 않는다)
2237
+ axmap --version 지금 도는 판 번호를 찍는다 (아래 version 명령과 다르다)
2200
2238
 
2201
2239
  딸린 프로그램 — 예전에는 파일 경로로 불렀다. 이제 이름으로 부른다.
2202
2240
  인자는 그대로 전달되고, 각각의 사용법은 그 프로그램이 답한다.
@@ -2219,6 +2257,27 @@ const HELP = `axmap - AI 에이전트 작업 선점 프로토콜
2219
2257
  const { positional, flags } = parseArgs(process.argv.slice(2))
2220
2258
  const [cmd, ...rest] = positional
2221
2259
 
2260
+ /**
2261
+ * 🔴 `--version` 은 명령이 아니라 **깃발**이라 아래 switch 에 안 걸린다.
2262
+ * 그대로 default 로 떨어져 **도움말을 찍고 종료 코드 0** 을 냈다. 팀 CI 가
2263
+ * "어느 판이 돌았는지" 를 남기려고 넣은 줄이 조용히 쓸모없어졌고, 0 이라
2264
+ * 아무도 못 알아챘다 (실측 2026-09-01, 팀 저장소 .gitlab-ci.yml eb3b56d).
2265
+ *
2266
+ * 🔴 `version` **명령과는 다른 것을 답한다.** 저쪽(tools/version.mjs)은
2267
+ * `process.cwd()` 의 git 태그를 읽는 도구라, 남의 저장소에서 부르면 그
2268
+ * 저장소의 버전이 나온다. 여기가 답해야 하는 것은 "지금 도는 axMap 이 몇
2269
+ * 판인가" 이고, 그 답은 설치본 자기 package.json 에만 있다.
2270
+ *
2271
+ * 못 읽으면 죽는다. 모르는 것을 빈 줄로 내면 CI 로그에는 "판을 확인했다" 는
2272
+ * 흔적만 남고 실제로는 아무것도 확인되지 않는다.
2273
+ */
2274
+ if (boolFlag(flags.version, 'version') && !cmd) {
2275
+ const v = selfVersion()
2276
+ if (!v) die('버전을 알 수 없습니다 - package.json 이 없는 사본입니다.')
2277
+ console.log(v)
2278
+ process.exit(0)
2279
+ }
2280
+
2222
2281
  switch (cmd) {
2223
2282
  // 이름을 경로로 바꿔 주는 것들 (RUNNERS). 인자는 손대지 않고 그대로 간다.
2224
2283
  case 'setup':
@@ -2238,6 +2297,9 @@ switch (cmd) {
2238
2297
  case 'init':
2239
2298
  cmdInit(flags)
2240
2299
  break
2300
+ case 'bus-repair':
2301
+ cmdBusRepair()
2302
+ break
2241
2303
  case 'claim':
2242
2304
  cmdClaim(rest, flags)
2243
2305
  break
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.0.0",
4
+ "version": "1.0.1",
5
5
  "//private": "🔴 여기 있던 \"private\": true 를 2026-08-28 에 지웠다. 그 줄이 있는 동안 npm publish 는 거부됐다. 되돌리려면 다시 넣으면 되지만, 이미 올라간 버전은 그래도 안 사라진다 — npm 은 같은 번호를 덮어쓰지 못하고 삭제도 72시간 안에만 된다. 즉 이 줄을 되살리는 것은 '앞으로 안 올린다' 는 뜻이지 '올린 것을 없앤다' 는 뜻이 아니다.",
6
6
  "//publishConfig": "axmap-cli 는 스코프가 없어서 기본이 이미 공개다. 즉 지금은 없어도 된다. 그래도 남겨 두는 이유: 나중에 @조직/axmap 처럼 스코프를 붙이는 날 이 줄이 없으면 publish 가 '유료 플랜이 필요하다'는 엉뚱한 말로 실패한다.",
7
7
  "publishConfig": {
package/tools/bus.mjs CHANGED
@@ -30,7 +30,7 @@
30
30
  * 에 한 줄로 적는 편이 빠르다 — 그건 `axmap status` 로 바로 보인다.
31
31
  */
32
32
 
33
- import { execFileSync } from 'node:child_process'
33
+ import { execFileSync, spawnSync } from 'node:child_process'
34
34
  import fs from 'node:fs'
35
35
  import path from 'node:path'
36
36
  import { fileURLToPath } from 'node:url'
@@ -114,6 +114,79 @@ function gitBus(argv, opts = {}) {
114
114
 
115
115
  const busReady = () => BUS_WT !== null && fs.existsSync(path.join(BUS_WT, '.git'))
116
116
 
117
+ /**
118
+ * 쪽지함을 못 연 이유. `null` 이면 정상이다.
119
+ *
120
+ * 🔴 이 변수가 있는 이유는 하나다 — **"쪽지 없음" 과 "못 읽었음" 을 다른 문장으로
121
+ * 내기 위해서다.**
122
+ *
123
+ * 2026-09-03 실측: 원격 `axmap/bus` 에 쪽지 129건이 있고 그중 오늘 온 것이
124
+ * 있는데, 이 체크아웃에 worktree 가 없어서 `pull()` 이 **fetch 를 한 번도 안
125
+ * 하고 조용히 반환**했다. 읽는 자리는 옛 함(`docs/bus`)으로 떨어졌고 거기 있던
126
+ * 8월 것은 이미 읽음이라 화면에 나온 답은 `쪽지 없음.` 이었다. 경고도 없었다.
127
+ *
128
+ * **보내기는 같은 상황에서 제대로 거부했다** (아래 `post`). 쓰기는 막고 읽기만
129
+ * 통과시킨 것이다. 락 시스템에서 최악의 실패는 거부해야 할 것을 조용히
130
+ * 통과시키는 것이고(CLAUDE.md 「애매하면 거부한다」), 쪽지함에서는 **못 읽은
131
+ * 것을 "없다" 고 답하는 것**이 바로 그 자리다. 잘못 막으면 사람이 메시지를
132
+ * 읽고 고치면 되지만, 조용히 없다고 하면 아무도 잘못됐다는 것을 모른다.
133
+ */
134
+ let busProblem = null
135
+
136
+ /** 옛 함(`docs/bus`)에서만 읽었는가. 새 함을 못 연 채로 성공한 것처럼 보이지 않게 한다. */
137
+ let readFromLegacyOnly = false
138
+
139
+ /**
140
+ * 쪽지함을 연다. **없으면 만든다. 만들었으면 만들었다고 말한다.**
141
+ *
142
+ * 🔴 만드는 코드를 여기에 복사하지 않는다. `bin/axmap.mjs` 의 `ensureBus` 하나뿐이고
143
+ * 이 함수는 그것을 `bus-repair` 로 부른다. 사본을 두면 두 벌이 되고, 두 벌은
144
+ * 반드시 어긋난다 — 이 저장소가 팀 사본을 걷어낸 것과 같은 이유다.
145
+ *
146
+ * 🔴 만들고 나서 **조용히 넘어가지 않는다.** 이 버그의 본질이 "말없이 넘어간 것"
147
+ * 이라, 자동으로 만들어 놓고 또 말없이 넘어가면 증상만 다른 같은 병이 된다.
148
+ * stdout 이 아니라 stderr 로 낸다 — 목록을 파이프로 넘겨도 사람 눈에는 남는다.
149
+ */
150
+ function openBus() {
151
+ if (process.env.AXMAP_BUS_DIR) return true // 대상이 명시됐으면 물을 것이 없다
152
+ if (busReady()) return true
153
+ if (BUS_WT === null) {
154
+ busProblem = '이 저장소를 찾지 못했습니다.'
155
+ return false
156
+ }
157
+
158
+ const r = spawnSync(process.execPath, [path.join(ROOT, 'bin', 'axmap.mjs'), 'bus-repair'], {
159
+ cwd: REPO, encoding: 'utf8', windowsHide: true,
160
+ })
161
+ if (busReady()) {
162
+ console.error(`쪽지함이 없어서 만들었습니다: ${path.join('.axmap', 'bus')} (${BUS_BRANCH})`)
163
+ return true
164
+ }
165
+ busProblem = (r.stderr ?? '').trim() || (r.stdout ?? '').trim() || '알 수 없는 이유로 실패했습니다.'
166
+ return false
167
+ }
168
+
169
+ /**
170
+ * 쪽지함을 못 열었다는 것을 사람이 볼 수 있게 낸다.
171
+ *
172
+ * 🔴 **"없음" 이라고 말하지 않는다.** 받은 쪽지가 없는 것이 아니라 확인을 못 한
173
+ * 것이고, 둘은 사람이 해야 할 일이 정반대다.
174
+ */
175
+ function reportBusProblem() {
176
+ // 🔴 순서가 뜻이다. **무슨 일인지 먼저, 이유는 그다음.** 이유부터 내면 그것이
177
+ // 여러 줄일 때 안내가 아래로 밀려서, 급한 사람이 첫 줄만 보고 넘어간다.
178
+ const why = busProblem
179
+ .replace(/^경고: /, '')
180
+ .split('\n')
181
+ .map((s, i) => (i === 0 ? s : ` ${s.trim()}`))
182
+ .join('\n')
183
+ console.error(
184
+ '쪽지함을 못 열었습니다 — 받은 쪽지가 없는 것이 아니라 확인을 못 한 것입니다.\n' +
185
+ `\n 이유: ${why}\n` +
186
+ '\n 고치기: axmap bus-repair (MCP 에서는 ax_init)',
187
+ )
188
+ }
189
+
117
190
  /**
118
191
  * 원격의 쪽지를 받아온다. **실패해도 죽지 않는다** — 못 받은 것은 위험이 아니라
119
192
  * 지연이다. 장부(`syncLedger`)가 같은 자리에서 죽는 것과 정반대이고, 그 차이의
@@ -129,7 +202,10 @@ const busReady = () => BUS_WT !== null && fs.existsSync(path.join(BUS_WT, '.git'
129
202
  * 쪽지 worktree(`.axmap/bus`) **바깥**이라 고아 브랜치에 섞이지 않는다.
130
203
  */
131
204
  function pull() {
132
- if (!busReady()) return
205
+ // 🔴 예전에는 여기가 `if (!busReady()) return` 이었다. 쪽지함이 없으면 **fetch 를
206
+ // 한 번도 안 하고 조용히 반환**했고, 그것이 "쪽지 없음" 으로 보였다.
207
+ // 이제는 열어 보고, 못 열면 그 사실을 `busProblem` 에 남긴다.
208
+ if (!openBus()) return
133
209
  // 🔴 주기는 **설정에서 온다.** `--throttle` 이 1순위(부르는 쪽이 그 자리에서
134
210
  // 정한다), 없으면 `AXMAP_BUS_POLL`(초), 그것도 없으면 0 = 매번 받아온다.
135
211
  //
@@ -230,11 +306,17 @@ const slug = (s) => s.toLowerCase().replace(/[^\w가-힣]+/g, '-').replace(/^-|-
230
306
 
231
307
  function readAll() {
232
308
  const found = []
309
+ let newBoxOpened = false
233
310
  for (const box of READ_BOXES) {
234
311
  let ns = []
235
312
  try { ns = fs.readdirSync(box).filter((f) => f.endsWith('.md')) } catch { continue }
313
+ if (box === BOX) newBoxOpened = true
236
314
  for (const f of ns) found.push({ box, f })
237
315
  }
316
+ // 🔴 옛 함(`docs/bus`)은 **읽기 전용 유산**이다. 새 함을 못 열었는데 옛 함이
317
+ // 읽히면 지금까지는 그것이 **성공한 것처럼** 보였다 — 8월 20일자 8건이
318
+ // 나오고 오늘 온 쪽지는 없는 것이 된다. 어느 함에서 읽었는지를 남긴다.
319
+ readFromLegacyOnly = !newBoxOpened && found.length > 0
238
320
  return found.map(({ box, f }) => {
239
321
  const text = fs.readFileSync(path.join(box, f), 'utf8')
240
322
  const head = {}
@@ -419,7 +501,25 @@ function list() {
419
501
  rows = rows.filter((m) => m.id > seen && m.from !== who)
420
502
  }
421
503
 
422
- if (!rows.length) return has('--quiet-if-empty') ? undefined : console.log('쪽지 없음.')
504
+ // 🔴 **못 읽었으면 "없음" 이라고 답하지 않는다.** 이 세 줄이 이 파일에서 제일
505
+ // 중요하다. 나머지는 편의고 이것만이 사고를 막는다.
506
+ //
507
+ // 종료 코드를 0 이 아닌 것으로 낸다 — MCP 서버(`mcp/server.mjs`)는 이미
508
+ // `ok: r.code === 0` 으로 갈라 "쪽지함을 읽지 못했습니다" 를 따로 내도록
509
+ // 되어 있었다. 읽기가 그 신호를 **한 번도 낸 적이 없었을** 뿐이다.
510
+ //
511
+ // `--quiet-if-empty`(훅·배너)일 때만 0 으로 끝낸다. 훅에서 0 이 아니면
512
+ // 커밋이 막히는데, 쪽지를 못 읽은 것으로 커밋을 막는 것은 과하다.
513
+ // **다만 말은 한다** — stderr 는 그 경우에도 그대로 나간다.
514
+ if (busProblem) {
515
+ reportBusProblem()
516
+ if (!has('--quiet-if-empty')) process.exit(1)
517
+ return
518
+ }
519
+ if (!rows.length) {
520
+ if (readFromLegacyOnly) console.error('※ 옛 쪽지함(docs/bus)만 읽었습니다 — 새 쪽지함은 비어 있습니다.')
521
+ return has('--quiet-if-empty') ? undefined : console.log('쪽지 없음.')
522
+ }
423
523
  for (const m of rows) {
424
524
  console.log(`${m.at?.slice(0, 16).replace('T', ' ')} ${(m.from ?? '?').padEnd(18)} → ${(m.to ?? 'all').padEnd(18)} ${m.subject ?? ''}`)
425
525
  console.log(` ${m.id}`)
@@ -444,6 +544,9 @@ function list() {
444
544
  function read(id) {
445
545
  pull()
446
546
  const m = readAll().find((x) => x.id === id || x.id.includes(id))
547
+ // 🔴 못 열었으면 "없는 쪽지" 라고 말하지 않는다. 목록과 같은 이유다 — 사람은
548
+ // 아이디를 잘못 적었다고 믿고 다시 치게 되는데, 몇 번을 쳐도 같은 답이 온다.
549
+ if (!m && busProblem) { reportBusProblem(); process.exit(1) }
447
550
  if (!m) { console.error(`없는 쪽지: ${id}`); process.exit(1) }
448
551
  console.log(`── ${m.subject}\n ${m.from} → ${m.to} ${m.at}\n`)
449
552
  console.log(m.body)
@@ -491,7 +594,11 @@ switch (cmd) {
491
594
  case 'post': post({ to: flag('--to'), subject: flag('--subject'), body: stdin() }); break
492
595
  case 'reply': {
493
596
  const target = args[1]
597
+ // 🔴 답장도 먼저 받아온다. 예전에는 받아오지 않고 찾아서, 원격에만 있는 쪽지에
598
+ // 답장하면 **"없는 쪽지"** 가 나왔다. 아이디를 잘못 적은 것과 구별이 안 된다.
599
+ pull()
494
600
  const src = readAll().find((x) => x.id === target || x.id.includes(target))
601
+ if (!src && busProblem) { reportBusProblem(); process.exit(1) }
495
602
  if (!src) { console.error(`없는 쪽지: ${target}`); process.exit(1) }
496
603
  post({ to: src.from, subject: flag('--subject') ?? `Re: ${src.subject}`, body: stdin(), replyTo: src.id })
497
604
  break
@@ -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
- }