axmap-cli 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,3 @@ description: axMap — 새 버전이 나왔는지 보고, 받을지는 사람이
14
14
 
15
15
  3. 종료 코드가 `1` 이면 **확인 자체를 못 한 것**이다 (네트워크, 또는 아직 배포된
16
16
  적이 없는 꾸러미). "최신입니다" 로 옮기지 마라 — 모르는 것과 최신인 것은 다르다.
17
-
18
- 팀 저장소 안의 `ci/axmap/` 사본에서 이 명령을 부르면 "사본입니다" 라고 답한다.
19
- 그건 정상이다. 사본은 npm 이 아니라 axMap 저장소의 `tools/vendor.mjs` 로 갱신한다.
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: axMap 세션·선점·받은 편지함을 터미널 카드로 보여 줍니다
3
+ ---
4
+
5
+ 현재 Git 저장소를 대상으로 별도의 대화형 터미널에서 `axmap view --cwd <현재 저장소 절대경로>`를 실행하도록 안내한다. 이 명령은 터미널에서 직접 갱신되며 `q`로 닫는다. 도구가 대화형 터미널을 제공하지 않는다면 `axmap view --cwd <현재 저장소 절대경로> --once`의 결과를 스냅샷으로 보여 주고 실시간 화면이라고 주장하지 않는다. 저장소가 모호하면 다른 저장소를 임의로 선택하지 않는다.
6
+
7
+ 관측은 작업을 새로 시작하거나 메시지를 회수·전송하지 않는다. 실제 등록·관측된 상태만 보여 주며 미확인 값을 추정하지 않는다.
8
+
9
+ 개발 중인 axMap 소스 체크아웃에서는 설치본이 구버전일 수 있다. `axmap view --help`를 확인하고 해당 명령이 없으면 소스 체크아웃의 `node bin/axmap.mjs view --cwd <현재 저장소 절대경로>`를 사용한다.
package/README.ko.md CHANGED
@@ -23,12 +23,29 @@ axmap setup
23
23
 
24
24
  지원되는 MCP Apps 호스트에서는 대화 안에서 파이프라인을 확인하고, Claude Code·agy에서는 기존 상태줄에 진행을 추가할 수 있습니다. 호스트별 지원이 다르며 ChatGPT·Claude 앱의 실제 임베디드 화면은 아직 검증하지 않았습니다. [클라이언트 설정과 제한](docs/PIPELINE-CLIENTS.md). 선택적으로 별도 터미널의 `axmap monitor`도 제공합니다([사용법](docs/MONITOR.md)).
25
25
 
26
+ 터미널에서 `axmap view --cwd <저장소>`를 실행하면 관측된 로컬 세션·선점·대기 메시지·최근 이벤트를 카드로 볼 수 있습니다. 화면은 자동 갱신되며 `q` 또는 Esc로 닫습니다. `--once`는 한 번만 출력하고 `--json`은 구조화된 결과를 제공합니다. 등록되지 않은 모든 에이전트나 GitLab 활동까지 표시하는 것은 아닙니다. [화면 사용법](docs/VIEW.md).
27
+
26
28
  ---
27
29
 
28
30
  ## 변경 기록
29
31
 
30
32
  최신 변경부터 정리합니다.
31
33
 
34
+ ### 1.9.0
35
+
36
+ <sub>2026.10.29</sub>
37
+
38
+ **작업 폴더에서 저장소를 찾고, 꺼진 로컬 세션을 알리며, 저장소 안 axMap 사본을 폐기합니다.**
39
+
40
+ - 작업 폴더가 Git 밖이면 `.axmap.json`(`{ "repo": "<경로>" }`) 수동 지정 → 하위 폴더 자동 탐색(모두 같은 저장소, worktree 포함) 순으로 대상을 정합니다. 서로 다른 저장소가 섞여 있으면 추측하지 않고 후보를 보여 줍니다. `axmap workspace show`로 판정 이유를 보고 `axmap workspace use <저장소>`로 기록합니다. 호출마다 다시 판정하므로 재시작이 필요 없습니다.
41
+ - 로컬 파이프가 사라진 관리형 세션은 대기 중이 아니라 `closed`로 표시합니다. 확인하지 못한 세션은 `liveness: unverified`로 구별하고 대기 중으로 보고하지 않습니다.
42
+ - 로컬 파이프에 연결조차 못 했거나 보내기 전에 거절된 요청은 `failed`로 기록해 다음 요청을 막지 않습니다. 이런 받은 편지함 묶음의 쪽지는 대기열로 돌아갑니다. 시간 초과와 연결 뒤 오류는 지금처럼 `unknown`이며 자동 재전송하지 않습니다.
43
+ - 팀 저장소에 axMap을 복사하던 방식(`tools/vendor.mjs` → `ci/axmap/`)을 폐기합니다. 커밋 훅은 사본으로 되돌아가지 않고, 사본만 남은 저장소에서는 `axmap hook install` 안내와 함께 멈춥니다. `axmap doctor`가 옛 훅을 표시합니다.
44
+
45
+ [결정 D30–D31 →](docs/DECISIONS.md)
46
+
47
+ ---
48
+
32
49
  ### 1.7.0
33
50
 
34
51
  <sub>2026.10.27</sub>
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # axMap
2
2
 
3
+ Run `axmap view --cwd <repository>` to see observed local sessions, claims, pending mailbox messages and recent events in a terminal dashboard. The view refreshes interactively; press `q` or Esc to exit. Use `--once` for a snapshot or `--json` for structured output. See [the view guide](docs/VIEW.md). The display reflects available observations, not a complete view of every agent or GitLab activity.
4
+
3
5
  **Several AI agents. One repository. Clear ownership before every edit.**
4
6
 
5
7
  axMap is a collaboration CLI that claims files and folders before work begins and rejects overlapping claims.
@@ -29,6 +31,21 @@ View pipeline state inside compatible MCP Apps hosts, or add it to the existing
29
31
 
30
32
  Newest changes first.
31
33
 
34
+ ### 1.9.0
35
+
36
+ <sub>October 29, 2026</sub>
37
+
38
+ **Find the repository from a workspace folder, report dead local sessions, and retire in-repository copies of axMap.**
39
+
40
+ - When the working folder is not inside Git, axMap picks the target repository in this order: a manual `.axmap.json` (`{ "repo": "<path>" }`), then automatic discovery of child folders that all belong to one repository (worktrees included). If child folders belong to different repositories, axMap does not guess; it lists the candidates. `axmap workspace show` explains the choice and `axmap workspace use <repo>` records it. The choice is re-evaluated on every call, so no restart is needed.
41
+ - Managed sessions whose local pipe no longer exists are reported as `closed` instead of idle. Sessions that cannot be checked are marked `liveness: unverified` and are not reported as idle.
42
+ - Requests that could not even connect to the local pipe, or were rejected before sending, are recorded as `failed` and no longer block the next request. Mailbox batches in that state return their messages to the queue. Timeouts and errors after connecting remain `unknown` and are never retried automatically.
43
+ - Copying axMap into a team repository (`tools/vendor.mjs` → `ci/axmap/`) is retired. Commit hooks no longer fall back to such a copy; if only a copy is present, the hook stops with instructions to run `axmap hook install`. `axmap doctor` flags old hooks.
44
+
45
+ [Decisions D30–D31 →](docs/DECISIONS.md)
46
+
47
+ ---
48
+
32
49
  ### 1.7.0
33
50
 
34
51
  <sub>October 27, 2026</sub>
package/bin/axmap.mjs CHANGED
@@ -36,7 +36,6 @@ import { auditLedger, formatAudit, planAudit, snapshotTime } from '../src/invari
36
36
  import {
37
37
  PACKAGE_NAME,
38
38
  fetchLatest,
39
- isVendored,
40
39
  checkDisabled,
41
40
  readCache,
42
41
  writeCache,
@@ -48,11 +47,27 @@ import { homeRegistrations } from '../src/mcpstate.mjs'
48
47
  import { compareBranchFor, formatOverlap } from '../src/overlap.mjs'
49
48
  import { withRepoLock } from '../src/repolock.mjs'
50
49
  import { readProfile, syncConfiguredProfile } from '../src/identity.mjs'
50
+ import { inspectTarget, ambiguityMessage, describeTarget, writeWorkspaceChoice } from '../src/workspace.mjs'
51
51
 
52
52
  /** 이 CLI 가 들어 있는 axMap 폴더. 갱신 확인과 setup 이 자기 위치를 알아야 한다. */
53
53
  const SELF_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
54
54
 
55
- /** 지금 도는 axMap 의 버전. 못 읽으면 `null` — 벤더링된 사본에는 package.json 이 없다. */
55
+ /**
56
+ * 이 폴더가 axMap 원본(소스 저장소 또는 npm 꾸러미)인가. `package.json` 의 이름으로 가른다.
57
+ *
58
+ * 팀 저장소 안에 복사해 두던 옛 사본(`ci/axmap` 등)에는 `package.json` 이 없었다.
59
+ * 그래서 훅이 "되돌아가도 되는 자리" 와 "걷어낸 사본" 을 이것 하나로 가른다 (D31).
60
+ * 못 읽으면 `false` — 원본이라고 판정하지 못했으면 원본으로 쓰지 않는다.
61
+ */
62
+ function isAxmapSourceRoot(dir) {
63
+ try {
64
+ return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))?.name === PACKAGE_NAME
65
+ } catch {
66
+ return false
67
+ }
68
+ }
69
+
70
+ /** 지금 도는 axMap 의 버전. 못 읽으면 `null` — 판을 지어내지 않는다. */
56
71
  function selfVersion() {
57
72
  try {
58
73
  return JSON.parse(fs.readFileSync(path.join(SELF_ROOT, 'package.json'), 'utf8'))?.version ?? null
@@ -523,8 +538,8 @@ function markBusSeen(root, me, id) {
523
538
  /**
524
539
  * 쪽지를 읽는 명령. **경로를 문자열로 적지 않고 계산한다.**
525
540
  *
526
- * 🔴 `node tools/bus.mjs …` 라고 적혀 있었다. 이 저장소에서는 맞고 **사본에서는
527
- * 틀린다** — 팀 저장소에 벤더링되면 그 파일은 `ci/axmap/tools/bus.mjs` 다.
541
+ * 🔴 `node tools/bus.mjs …` 라고 적혀 있었다. axMap 저장소에서는 맞고 **설치본이나
542
+ * 다른 저장소에서는 틀린다** — 그 파일은 도구가 놓인 자리에 있다.
528
543
  * 안내대로 치면 `MODULE_NOT_FOUND` 가 난다. 알림이 "쪽지가 있다" 고 말한 직후에
529
544
  * 읽는 방법을 틀리게 알려주는 것이라, 받은 사람은 도구가 고장 났다고 결론짓는다.
530
545
  */
@@ -2396,15 +2411,24 @@ function cmdDoctor(flags) {
2396
2411
  const hookAbs = path.isAbsolute(hookPath) ? hookPath : path.join(root, hookPath)
2397
2412
  if (fs.existsSync(hookAbs) && fs.readFileSync(hookAbs, 'utf8').includes('axmap')) {
2398
2413
  // 훅은 절대 경로를 담는다. 폴더 이름이 바뀌면 사라진 곳을 가리킨다.
2399
- // 다만 요즘 훅은 그 경로가 없을 때 저장소 안을 한 번 더 본다(cmdHookInstall 참조).
2400
- // 그래서 박아둔 경로만 보고 빨간불을 켜면 **거짓 경보**가 된다.
2401
- const target = (fs.readFileSync(hookAbs, 'utf8').match(/"([^"]+axmap\.mjs)"/) ?? [])[1]
2402
- const fallbacks = ['ci/axmap/bin/axmap.mjs', 'axmap/bin/axmap.mjs', 'bin/axmap.mjs']
2403
- .map((rel) => path.join(root, rel))
2404
- .filter((p) => fs.existsSync(p))
2405
- if (!target || fs.existsSync(target)) ok('커밋 훅', '심겨 있습니다')
2406
- else if (fallbacks.length) ok('커밋 훅', `심겨 있습니다 (박아둔 경로 대신 ${path.relative(root, fallbacks[0]).replace(/\\/g, '/')} 를 씁니다)`)
2407
- else no('커밋 훅', `가리키는 파일이 없습니다 (저장소 안에도 없습니다): ${target}\n axmap hook install 로 다시 심으세요`)
2414
+ // 다만 요즘 훅은 그 경로가 없을 때 axMap 저장소 자신(<루트>/bin)을 한 번 더 본다
2415
+ // (cmdHookInstall 참조). 그래서 박아둔 경로만 보고 빨간불을 켜면 **거짓 경보**가 된다.
2416
+ //
2417
+ // 🔴 저장소 안 사본(ci/axmap · axmap)은 2026-10-29 에 걷어냈다 (DECISIONS D31).
2418
+ // 그 전에 심은 훅은 박아둔 경로가 없으면 그 사본으로 되돌아가 **옛 규칙으로**
2419
+ // 판정한다. 겉으로는 커밋이 잘 되므로 여기서 말하지 않으면 아무도 모른다.
2420
+ const hookText = fs.readFileSync(hookAbs, 'utf8')
2421
+ const target = (hookText.match(/"([^"]+axmap\.mjs)"/) ?? [])[1]
2422
+ // 옛 훅은 둘 중 하나다: 사본을 직접 박았거나(`node ci/axmap/bin/axmap.mjs hook install`),
2423
+ // 되돌아갈 자리 목록(`for p in ...`)에 사본을 넣었다. 지금 훅도 "사본이 있으면 막는다"
2424
+ // 는 검사에 같은 경로 글자를 쓰므로, 글자가 들어 있다는 것만으로는 가르지 않는다.
2425
+ const pinsCopy = /[\\/](ci[\\/])?axmap[\\/]bin[\\/]axmap\.mjs$/.test(target ?? '') && !isAxmapSourceRoot(path.resolve(target, '..', '..'))
2426
+ const legacy = pinsCopy || /for p in "\$ROOT\/ci\/axmap\/bin\/axmap\.mjs"/.test(hookText)
2427
+ const selfFallback = isAxmapSourceRoot(root) && fs.existsSync(path.join(root, 'bin', 'axmap.mjs'))
2428
+ if (legacy) no('커밋 훅', '옛 훅입니다 — 저장소 안 axMap 사본(ci/axmap)을 부르거나 그리로 되돌아갑니다.\n 사본은 옛 규칙으로 판정하고, axMap 은 더 이상 사본을 쓰지 않습니다.\n axmap hook install 로 다시 심으세요')
2429
+ else if (!target || fs.existsSync(target)) ok('커밋 훅', '심겨 있습니다')
2430
+ else if (selfFallback) ok('커밋 훅', '심겨 있습니다 (박아둔 경로 대신 bin/axmap.mjs 를 씁니다)')
2431
+ else no('커밋 훅', `가리키는 파일이 없습니다: ${target}\n axmap hook install 로 다시 심으세요`)
2408
2432
  } else {
2409
2433
  no('커밋 훅', `없습니다 — claim 하지 않은 파일도 그냥 커밋됩니다\n axmap hook install`)
2410
2434
  }
@@ -2456,15 +2480,11 @@ function cmdDoctor(flags) {
2456
2480
  // doctor 는 "지금 도는가" 를 재는 자리이고, 레지스트리를 기다리게 만들면
2457
2481
  // 네트워크가 느린 날 점검 자체가 안 끝난다.
2458
2482
  const ver = selfVersion()
2459
- if (isVendored(SELF_ROOT)) {
2460
- ok('버전', `${ver ?? '(모름)'} — 벤더링된 사본입니다. npm 이 아니라 tools/vendor.mjs 가 갱신합니다`)
2461
- } else {
2462
- const cache = readCache()
2463
- const notice = noticeLine(ver, cache?.latest)
2464
- if (notice) hm('새 버전', notice.split('\n').join('\n '))
2465
- else if (cache) ok('버전', `${ver ?? '(모름)'} — 최신입니다 (확인: ${new Date(cache.checkedAt).toISOString()})`)
2466
- else ok('버전', `${ver ?? '(모름)'} — 새 버전이 있는지 물어본 적이 없습니다 (axmap update)`)
2467
- }
2483
+ const cache = readCache()
2484
+ const notice = noticeLine(ver, cache?.latest)
2485
+ if (notice) hm('새 버전', notice.split('\n').join('\n '))
2486
+ else if (cache) ok('버전', `${ver ?? '(모름)'} — 최신입니다 (확인: ${new Date(cache.checkedAt).toISOString()})`)
2487
+ else ok('버전', `${ver ?? '(모름)'} — 새 버전이 있는지 물어본 적이 없습니다 (axmap update)`)
2468
2488
 
2469
2489
  return finishDoctor(lines, bad, warn, flags)
2470
2490
  }
@@ -2498,57 +2518,55 @@ function cmdHookInstall() {
2498
2518
  * 🔴 박아둔 경로를 **1순위로 두되 유일한 길로 두지 않는다.**
2499
2519
  *
2500
2520
  * 위 주석의 의도(바깥 설치 대응)는 옳다. 그래서 `self` 를 그대로 첫 번째로 둔다.
2501
- * 그런데 저장소가 갈래마다 axMap 을 다른 자리에 두면 — 원본은 `axmap/`, 벤더링
2502
- * 사본은 `ci/axmap/` — 브랜치를 옮기는 순간 박아둔 경로가 **사라진다.**
2503
- *
2504
- * 문제는 사라졌다는 사실이 아니라 **죽는 방식**이다. 훅은 "검사 실패" 가 아니라
2505
- * `MODULE_NOT_FOUND` 로 죽고, 0 이 아닌 종료 코드는 곧 커밋 차단이다. 즉
2506
- * **선점과 무관한 커밋까지 전부 막힌다.** 막히면 사람은 계속 일해야 하므로
2507
- * `--no-verify` 를 쓰기 시작하고, **그 습관은 훅이 고쳐진 뒤에도 남는다.**
2508
- * 지금의 고장보다 그것이 비싸다.
2521
+ * 박아둔 경로가 사라지면 훅은 "검사 실패" 가 아니라 `MODULE_NOT_FOUND` 로 죽고,
2522
+ * 0 이 아닌 종료 코드는 곧 커밋 차단이다 — **선점과 무관한 커밋까지 전부 막힌다.**
2523
+ * 막히면 사람은 `--no-verify` 를 쓰기 시작하고, 그 습관은 훅이 고쳐진 뒤에도 남는다.
2509
2524
  *
2510
- * 그래서 넷을 차례로 본다. **배치가 셋이라는 것은 브랜치를 훑어 실측한 것이다**
2511
- * (2026-08-27 · `git cat-file -e <브랜치>:<경로>`):
2525
+ * 그래서 둘을 차례로 본다.
2512
2526
  *
2513
- * 1. 박아둔 절대 경로 (바깥 설치)
2514
- * 2. <루트>/ci/axmap/bin main · common/dev · bigData/dev ← 현재 표준
2515
- * 3. <루트>/axmap/bin chore/axmap-bootstrap · backup/*
2516
- * 4. <루트>/bin back/dev · front/dev ← 옛 배치
2527
+ * 1. 박아둔 절대 경로 (npm 설치본 · 바깥 설치)
2528
+ * 2. <루트>/bin/axmap.mjs **axMap 저장소 자신일 때만** (폴더를 옮긴 경우)
2517
2529
  *
2518
- * 🔴 4번을 빠뜨리면 **도구가 거기 있는데 못 찾아서 통과한다.** 그건 "설치 안 됨"
2519
- * 이 아니라 "찾는 목록이 짧음" 이고, 아래 fail-open 의 근거가 성립하지 않는
2520
- * 경우다. 하필 `back/dev` · `front/dev` 는 BE·FE 가 앞으로 실제로 쓸 갈래다.
2530
+ * 🔴 **저장소 안 사본(`ci/axmap` · `axmap`)은 보지 않는다 (2026-10-29, DECISIONS D31).**
2531
+ * 예전에는 그 둘도 되돌아갈 자리였다. 그런데 사본은 만든 날의 규칙에 멈춰 있어서,
2532
+ * 박아둔 경로가 사라진 날 훅이 **옛 규칙으로 판정**했다 (GIT-AS-DB 사고 3).
2533
+ * axMap 은 이제 장부와 코드를 한 곳에만 둔다. 2번이 "axMap 저장소 자신" 인지는
2534
+ * `package.json` 의 꾸러미 이름으로 가른다 — 팀 저장소의 옛 `bin/` 사본에는 그
2535
+ * 파일이 없다.
2521
2536
  *
2522
- * 🔴 셋 다 없으면 **시끄럽게 경고하고 통과시킨다(exit 0).**
2537
+ * 🔴 사본은 있는데 진짜 도구를 못 찾으면 **막는다(exit 1).** 사본으로 되돌아가던
2538
+ * 사람에게 그냥 통과시키면 어제까지 막히던 커밋이 오늘 조용히 통과한다 — 판정을
2539
+ * 못 했는데 통과시키는 것이다. 무엇을 하면 되는지(`hook install`)를 같이 말한다.
2523
2540
  *
2524
- * "도구를 못 찾은 것" 과 "검사가 실패한 것" 은 다르다. 도구가 있는데 선점을
2525
- * 안 했으면 반드시 막아야 하지만, 도구를 못 찾은 것은 설치 문제다. 설치 문제로
2526
- * **선점과 무관한 커밋까지 전부 막으면** 사람은 `--no-verify` 를 쓰기 시작하고,
2527
- * 그 습관은 훅이 고쳐진 뒤에도 남는다. 그때는 진짜로 아무것도 안 막힌다.
2528
- * `ax_inbox` 의 "없는 것과 못 읽은 것은 다르다" 와 같은 결이다.
2529
- *
2530
- * ⚠ 다만 **CI 가 대신 잡아 준다고 말하지 않는다.** 팀 저장소의 `claims` 잡은
2531
- * `merge_request_event` 에서만 돌고 `changes:` 목록에 든 폴더만 본다.
2532
- * dev 브랜치 직접 push 와 목록 밖 폴더는 그물 밖이다. 그래서 여기서 할 수
2533
- * 있는 것은 **조용히 넘어가지 않는 것**뿐이고, 문구는 그 사실만 말한다.
2534
- * (그물을 넓히는 것은 `.gitlab-ci.yml` 쪽 일이라 여기서 처리하지 않는다.)
2541
+ * 🔴 사본도 도구도 없으면 **시끄럽게 경고하고 통과시킨다(exit 0).** 이건 설치 문제다.
2542
+ * 설치 문제로 선점과 무관한 커밋까지 전부 막으면 위의 `--no-verify` 습관이 생긴다.
2543
+ * ⚠ 다만 **CI 가 대신 잡아 준다고 말하지 않는다.** 여기서 할 수 있는 것은
2544
+ * **조용히 넘어가지 않는 것**뿐이고, 문구는 그 사실만 말한다.
2535
2545
  */
2536
2546
  const body = `#!/bin/sh
2537
2547
  # axMap - claim 하지 않은 파일의 커밋을 막는다.
2538
- # 경로를 하나만 박지 않는다 — 저장소가 갈래마다 axMap 을 다른 자리에 두기 때문이다
2539
- # (ci/axmap · axmap · bin 세 배치). 자세한 근거는 bin/axmap.mjs 의 cmdHookInstall.
2548
+ # 박아둔 경로가 없으면 axMap 저장소 자신(<루트>/bin)만 한 번 더 본다.
2549
+ # 저장소 안 사본(ci/axmap)은 쓰지 않는다. 자세한 근거는 bin/axmap.mjs 의 cmdHookInstall.
2540
2550
  AX="${self}"
2541
- if [ ! -f "$AX" ]; then
2542
- ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || ROOT=""
2543
- for p in "$ROOT/ci/axmap/bin/axmap.mjs" "$ROOT/axmap/bin/axmap.mjs" "$ROOT/bin/axmap.mjs"; do
2544
- if [ -f "$p" ]; then AX="$p"; break; fi
2545
- done
2551
+ ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || ROOT=""
2552
+ if [ ! -f "$AX" ] && [ -n "$ROOT" ] && [ -f "$ROOT/bin/axmap.mjs" ] && grep -q '"name": *"${PACKAGE_NAME}"' "$ROOT/package.json" 2>/dev/null; then
2553
+ AX="$ROOT/bin/axmap.mjs"
2546
2554
  fi
2547
2555
  if [ ! -f "$AX" ]; then
2556
+ if [ -n "$ROOT" ] && { [ -f "$ROOT/ci/axmap/bin/axmap.mjs" ] || [ -f "$ROOT/axmap/bin/axmap.mjs" ]; }; then
2557
+ echo "" >&2
2558
+ echo " !! axMap: 이 저장소 안의 axMap 사본(ci/axmap)은 더 이상 쓰지 않습니다." >&2
2559
+ echo " 사본은 옛 규칙으로 판정하므로 검사를 맡기지 않았고, 이 커밋을 멈췄습니다." >&2
2560
+ echo " 고치려면 - 이 저장소에서 axmap hook install" >&2
2561
+ echo " (axmap 명령이 없으면 먼저 npm install -g ${PACKAGE_NAME}@latest)" >&2
2562
+ echo "" >&2
2563
+ exit 1
2564
+ fi
2548
2565
  echo "" >&2
2549
2566
  echo " !! axMap: 검사 도구를 찾지 못했습니다." >&2
2550
2567
  echo " 이 커밋은 선점 검사를 거치지 않았습니다." >&2
2551
- echo " 고치려면 - node <axmap 위치>/bin/axmap.mjs hook install" >&2
2568
+ echo " 고치려면 - 이 저장소에서 axmap hook install" >&2
2569
+ echo " (axmap 명령이 없으면 먼저 npm install -g ${PACKAGE_NAME}@latest)" >&2
2552
2570
  echo "" >&2
2553
2571
  exit 0
2554
2572
  fi
@@ -2600,14 +2618,10 @@ exec node "$AX" verify
2600
2618
  * 🔴 **import 하지 않고 spawn 한다.** 이유가 둘이다.
2601
2619
  *
2602
2620
  * 1. `tools/setup.mjs` 는 설치기를 부르느라 `src/agentcli.mjs` 계열까지
2603
- * 끌고 온다. 여기서 static import 를 걸면 그 전부가 `src/closure.mjs` 의
2604
- * import 그래프에 잡혀 **팀 저장소의 `ci/axmap/` 사본이 통째로 불어난다.**
2605
- * 사본은 CI 가 판정에 쓰는 파일만 담기로 한 자리다.
2621
+ * 끌고 온다. static import 를 걸면 CLI 를 띄울 때마다 그 전부를 읽는다.
2606
2622
  * 2. 설치는 판정이 아니다. 실패해도 CLI 의 나머지가 멈출 이유가 없다.
2607
2623
  *
2608
- * 그 대가로 **사본에는 이 파일이 없다.** 없을 때 조용히 실패하지 않고, 왜 없는지와
2609
- * 무엇을 대신 하면 되는지를 말한다 — 이 저장소가 `tools/bus.mjs` 로 한 번 겪은
2610
- * 실패다(사본에 빠져 있는데 오류만 났다).
2624
+ * 파일이 없을 때는 조용히 실패하지 않고, 꾸러미가 온전하지 않다고 말한다.
2611
2625
  */
2612
2626
  /**
2613
2627
  * 딸린 프로그램들. **이름을 경로로 바꿔 주는 것이 전부다.**
@@ -2617,6 +2631,7 @@ exec node "$AX" verify
2617
2631
  * **npm 으로 설치하면 그 경로가 기계마다 다르다.** 그래서 팀의 CI 설정과
2618
2632
  * 슬래시 명령이 경로를 박아 둘 수밖에 없었고, 경로가 박혀 있는 한 사본을
2619
2633
  * 지울 수 없었다. 여기 이름이 하나 생길 때마다 박힌 경로가 하나 사라진다.
2634
+ * 이 이름들이 자리를 잡은 뒤 사본은 2026-10-29 에 걷어냈다 (DECISIONS D31).
2620
2635
  *
2621
2636
  * 값은 [파일, 한 줄 설명] 이다. 설명은 HELP 와 오류 메시지가 같이 쓴다 —
2622
2637
  * 두 군데 적으면 한 쪽만 고쳐지는 날이 온다.
@@ -2625,6 +2640,7 @@ const RUNNERS = {
2625
2640
  run: ['tools/run.mjs', '사용자가 여는 AI CLI를 연결한다'],
2626
2641
  bridge: ['tools/bridge.mjs', '열린 로컬 AI 세션에 작업을 전달한다'],
2627
2642
  monitor: ['tools/monitor.mjs', '세션과 요청 파이프라인을 실시간으로 표시한다'],
2643
+ view: ['tools/view.mjs', '세션·선점·받은 편지함을 터미널 카드로 표시한다'],
2628
2644
  pipeline: ['tools/pipeline.mjs', '대화 안 파이프라인 표시와 CLI 상태줄을 연결한다'],
2629
2645
  gate: ['governance/gate.mjs', '이 MR 이 정족수를 채웠는가'],
2630
2646
  vote: ['governance/vote.mjs', '표를 던진다'],
@@ -2642,11 +2658,7 @@ function cmdRun(name) {
2642
2658
  if (!fs.existsSync(script)) {
2643
2659
  die(
2644
2660
  `이 axMap 에는 ${name}(${what})이 들어 있지 않습니다: ${script}\n\n` +
2645
- (isVendored(SELF_ROOT)
2646
- ? ` 여기는 팀 저장소 안의 **벤더링된 사본**입니다(옆에 SOURCE.json 이 있습니다).\n` +
2647
- ` 사본에는 CI 가 판정에 쓰는 파일만 들어 있습니다.\n\n` +
2648
- ` 배포판으로 부르세요:\n npx -y ${PACKAGE_NAME}@latest ${name}`
2649
- : ` 꾸러미가 온전하지 않습니다. 다시 받으세요:\n npx -y ${PACKAGE_NAME}@latest ${name}`),
2661
+ ` 꾸러미가 온전하지 않습니다. 다시 받으세요:\n npx -y ${PACKAGE_NAME}@latest ${name}`,
2650
2662
  )
2651
2663
  }
2652
2664
  // 인자는 그대로 넘긴다. 여기서 다시 해석하면 두 곳의 플래그 목록이 어긋난다.
@@ -2689,20 +2701,6 @@ function cmdRun(name) {
2689
2701
  async function cmdUpdate(flags) {
2690
2702
  const current = selfVersion()
2691
2703
 
2692
- // 사본은 npm 이 아니라 tools/vendor.mjs 가 갱신한다. 여기서 레지스트리를 치면
2693
- // 팀 CI 가 매번 바깥 네트워크에 의존하게 되고, 안내를 받은 팀원이 할 수 있는
2694
- // 일도 없다 (사본은 axMap 저장소에서 vendor.mjs 를 다시 돌려야 바뀐다).
2695
- if (isVendored(SELF_ROOT)) {
2696
- console.log('')
2697
- console.log('여기는 팀 저장소 안의 벤더링된 사본입니다 (옆에 SOURCE.json 이 있습니다).')
2698
- console.log('사본은 npm 이 아니라 axMap 저장소의 tools/vendor.mjs 가 갱신합니다.')
2699
- console.log('')
2700
- console.log(' 사본이 온전한지: node ci/verify-vendor.mjs')
2701
- console.log(' 어느 커밋의 사본인지: ci/axmap/SOURCE.json')
2702
- console.log('')
2703
- process.exit(0)
2704
- }
2705
-
2706
2704
  const registry = registryUrl()
2707
2705
  console.log('')
2708
2706
  console.log(`지금 버전: ${current ?? '(모름)'}`)
@@ -2773,8 +2771,8 @@ async function cmdUpdate(flags) {
2773
2771
  * 출력이 **돌리는 사람의 PC 상태에 따라 달라지고**, 그러면 시험이 사람마다
2774
2772
  * 다른 답을 낸다 (실제로 그렇게 깨졌다).
2775
2773
  *
2776
- * 🔴 못 읽으면 `null` 을 돌려 **아무 말도 안 한다.** 사본을 안 쓰는 설치도 있고
2777
- * (팀 저장소 안의 벤더링 사본은 위에서 이미 걸러진다), 없는 것을 낡았다고
2774
+ * 🔴 못 읽으면 `null` 을 돌려 **아무 말도 안 한다.** 사본을 안 쓰는 설치도 있고,
2775
+ * 없는 것을 낡았다고
2778
2776
  * 말하면 그 경고는 곧 아무도 안 읽는 잡음이 된다.
2779
2777
  */
2780
2778
  function installedAppVersion() {
@@ -2797,13 +2795,69 @@ function installedAppVersion() {
2797
2795
  * 글을 섞으면 그 출력을 파싱하는 쪽이 조용히 깨진다.
2798
2796
  */
2799
2797
  function warnIfStale() {
2800
- if (checkDisabled() || isVendored(SELF_ROOT)) return
2798
+ if (checkDisabled()) return
2801
2799
  const cache = readCache()
2802
2800
  if (!cache) return
2803
2801
  const notice = noticeLine(selfVersion(), cache.latest)
2804
2802
  if (notice) console.error(`\n${notice}\n`)
2805
2803
  }
2806
2804
 
2805
+ // ---------------------------------------------------------------------------
2806
+ // 명령: workspace (Git 저장소가 아닌 작업 폴더에서 대상 저장소를 보고 정한다)
2807
+ // ---------------------------------------------------------------------------
2808
+
2809
+ /**
2810
+ * 장부를 쓰는 명령은 Git 저장소 안에서 돈다. 터미널이 여러 저장소(worktree)를 담은
2811
+ * 작업 폴더에서 열렸으면 MCP 서버·로컬 세션 연결과 **같은 규칙**(src/workspace.mjs)으로
2812
+ * 대상 저장소를 정하고 그 최상위로 옮겨서 실행한다. 선점 경로는 저장소 기준 경로다.
2813
+ *
2814
+ * 🔴 빠른 길: 지금 폴더나 조상에 `.git` 이 있으면 git 을 한 번도 더 띄우지 않고
2815
+ * 지금까지와 똑같이 돈다. 거의 모든 명령이 여기를 지나므로 Git 안의 사용자에게
2816
+ * 비용을 늘리지 않는다 (ledgerlock 테스트의 시간 창이 그 비용에 민감하다).
2817
+ */
2818
+ const WORKSPACE_COMMANDS = new Set(['init', 'bus-repair', 'users-repair', 'claim', 'release', 'renew', 'status', 'audit', 'doctor', 'hook', 'bus'])
2819
+
2820
+ function insideGitMarker(dir) {
2821
+ for (let at = path.resolve(dir); ; at = path.dirname(at)) {
2822
+ if (fs.existsSync(path.join(at, '.git'))) return true
2823
+ if (path.dirname(at) === at) return false
2824
+ }
2825
+ }
2826
+
2827
+ function enterWorkspaceTarget(name) {
2828
+ if (!WORKSPACE_COMMANDS.has(name) || process.env.GIT_DIR || insideGitMarker(process.cwd())) return
2829
+ let r
2830
+ try { r = inspectTarget(process.cwd()) } catch (e) { die(e.message) }
2831
+ if (r.kind === 'ambiguous') die(ambiguityMessage(r))
2832
+ if (r.kind !== 'manual' && r.kind !== 'auto') return // 저장소 없음 → 기존 안내(repoRoot)
2833
+ console.error(`axMap: 작업 폴더가 Git 저장소가 아니라 ${r.kind === 'manual' ? '수동 설정' : '자동 탐색'}으로 ${r.repo} 를 대상으로 합니다.`)
2834
+ process.chdir(r.repo)
2835
+ }
2836
+
2837
+ function cmdWorkspace(rest, flags) {
2838
+ const [sub, target] = rest
2839
+ if (sub === 'show') {
2840
+ let r
2841
+ try { r = inspectTarget(target ? path.resolve(target) : process.cwd()) } catch (e) { die(e.message) }
2842
+ if (boolFlag(flags.json, 'json')) {
2843
+ const out = { kind: r.kind, from: r.from, repo: r.repo ?? null, config: r.config ?? null,
2844
+ candidates: r.kind === 'ambiguous' ? r.groups.map(g => ({ repo: g.target, folders: g.members.map(m => m.name) }))
2845
+ : r.kind === 'auto' ? [{ repo: r.group.target, folders: r.group.members.map(m => m.name) }] : [] }
2846
+ console.log(JSON.stringify(out, null, 2))
2847
+ } else console.log(describeTarget(r))
2848
+ // 모호함은 "아직 정하지 않았다" 는 결과라 성공으로 세지 않는다.
2849
+ process.exit(r.kind === 'ambiguous' ? 2 : 0)
2850
+ }
2851
+ if (sub === 'use') {
2852
+ if (!target) die('사용법: axmap workspace use <저장소 폴더>')
2853
+ let w
2854
+ try { w = writeWorkspaceChoice(process.cwd(), target) } catch (e) { die(e.message) }
2855
+ console.log(`${w.config} 에 저장했습니다: { "repo": "${w.value}" }\n대상 저장소: ${w.repo}\nMCP 서버는 다음 도구 호출부터 따릅니다. 다시 켤 필요가 없습니다.`)
2856
+ return
2857
+ }
2858
+ die('사용법: axmap workspace show [폴더] [--json] | axmap workspace use <저장소 폴더>')
2859
+ }
2860
+
2807
2861
  // ---------------------------------------------------------------------------
2808
2862
 
2809
2863
  const HELP = `axmap - AI 에이전트 작업 선점 프로토콜
@@ -2826,6 +2880,9 @@ const HELP = `axmap - AI 에이전트 작업 선점 프로토콜
2826
2880
  [--checkpoint 통과하면 "여기까지 봤다" 를 남긴다]
2827
2881
  axmap doctor 이 PC 에서 선점이 실제로 도는지 점검 [--json]
2828
2882
  axmap hook install verify 를 pre-commit 훅으로 설치
2883
+ axmap workspace show [폴더] Git 저장소가 아닌 작업 폴더에서 어느 저장소를 겨누는지와 이유
2884
+ [--json] (수동 설정 · 자동 탐색 · 모호함 + 후보)
2885
+ axmap workspace use <저장소 폴더> 그 선택을 이 폴더의 .axmap.json 에 적는다
2829
2886
  axmap update 새 버전이 있는지 묻는다 (바꾸지는 않는다)
2830
2887
  axmap --version 지금 도는 판 번호를 찍는다 (아래 version 명령과 다르다)
2831
2888
 
@@ -2837,6 +2894,7 @@ const HELP = `axmap - AI 에이전트 작업 선점 프로토콜
2837
2894
  axmap bus 에이전트 사이 쪽지함 (post · list · read · reply)
2838
2895
  axmap bridge 로컬 세션·자동 대화 (serve · sessions · send · chat · dialogue)
2839
2896
  axmap monitor 에이전트·요청 파이프라인·최근 이벤트 실시간 화면
2897
+ axmap view 세션·선점·받은 편지함을 카드로 실시간 표시
2840
2898
  axmap pipeline 대화 안 표시·Claude/agy 상태줄 연결 (--help)
2841
2899
  axmap run agy [인수...] 연결된 Antigravity CLI를 직접 연다
2842
2900
  axmap mr-target 이 브랜치가 저 브랜치로 갈 수 있는가
@@ -2871,16 +2929,22 @@ const [cmd, ...rest] = positional
2871
2929
  */
2872
2930
  if (boolFlag(flags.version, 'version') && !cmd) {
2873
2931
  const v = selfVersion()
2874
- if (!v) die('버전을 알 수 없습니다 - package.json 이 없는 사본입니다.')
2932
+ if (!v) die('버전을 알 수 없습니다 - 이 axMap 옆에 package.json 이 없습니다.')
2875
2933
  console.log(v)
2876
2934
  process.exit(0)
2877
2935
  }
2878
2936
 
2937
+ enterWorkspaceTarget(cmd)
2938
+
2879
2939
  switch (cmd) {
2940
+ case 'workspace':
2941
+ cmdWorkspace(rest, flags)
2942
+ break
2880
2943
  // 이름을 경로로 바꿔 주는 것들 (RUNNERS). 인자는 손대지 않고 그대로 간다.
2881
2944
  case 'setup':
2882
2945
  case 'bridge':
2883
2946
  case 'monitor':
2947
+ case 'view':
2884
2948
  case 'pipeline':
2885
2949
  case 'run':
2886
2950
  case 'gate':
package/docs/VIEW.md ADDED
@@ -0,0 +1,19 @@
1
+ # 터미널에서 axMap 보기
2
+
3
+ 설치한 패키지에서 다음 명령으로 실행한다.
4
+
5
+ ```powershell
6
+ axmap view --cwd C:\work\my-repo
7
+ ```
8
+
9
+ 소스 체크아웃에서 확인할 때는 `node bin/axmap.mjs view --cwd <repo>`를 사용한다.
10
+
11
+ 연결된 세션을 우선해 카드로 보여 주고 현재 선점, 등록된 세션의 대기 중인 로컬 편지와 최근 관측 이벤트를 표시한다. 실시간 갱신 중 `q`, Esc 또는 Ctrl+C로 화면을 닫는다. 진행 중인 작업은 계속된다. 창 크기에 따라 카드가 한 줄 또는 여러 줄로 배치된다.
12
+
13
+ `--once`는 한 번만 출력하며, `--json`은 같은 관측 자료를 JSON으로 출력한다. `--interval 1000`은 수집 간격이고 `--no-color` 또는 `NO_COLOR`는 색을 끈다. 파이프나 리디렉션에서는 스냅샷만 출력한다. 터미널 글꼴은 사용자의 터미널 설정을 따른다.
14
+
15
+ 수신자별 편지함을 읽기만 한다. 읽음 처리, 만료 상태 저장, 메시지 전송, 모델 요청 및 승인 조작은 하지 않는다. 만료 시각이 지난 대기 메시지는 화면 집계에서 제외하고 파일은 수정하지 않는다. 편지함을 읽을 수 없으면 빈 편지함으로 단정하지 않고 오류를 보여 준다. 기존 모니터와 마찬가지로 요청의 관측 결과 확인 과정은 기존 요청 기록을 갱신할 수 있다. 출력에 메시지 주제 또는 내용이 포함될 수 있으니 공유할 때 범위를 확인한다.
16
+
17
+ 포트폴리오 그림은 설명용 애니메이션이며 이 명령은 실제 관측값을 보여 준다. 확인하지 못한 작업률·토큰·연결 상태를 임의로 만들지 않는다. 등록하지 않은 AI 대화는 표시되지 않는다.
18
+
19
+ Claude Code에서 프로젝트 명령을 설치한 환경은 `/axmap-view`로 안내를 부를 수 있다. Codex 등 다른 호스트는 `skills/axmap-view/SKILL.md`를 해당 호스트의 방법으로 설치해야 하며, 모든 터미널에서 `/axmap-view` 자체가 실행되는 것은 아니다. 확실한 실행 방법은 `axmap view`다.
@@ -19,7 +19,7 @@
19
19
  * `agent.summary` AI 가 적는다 (`--agent-summary`)
20
20
  * `agent.id` 부르는 쪽이 적는다 (`--agent`, 없으면 `"none"`)
21
21
  * `agent.shown` 🔴 **아무도 못 적는다. 이 프로그램이 직접 잰다** —
22
- * 변경량·경로·그 시점의 판정·axMap 사본이 몇 커밋 뒤처졌는지.
22
+ * 변경량·경로·그 시점의 판정.
23
23
  * `committerEmail` 을 못 적게 한 것과 같은 이유다 (아래).
24
24
  *
25
25
  * GitLab 의 Approve 버튼을 쓰지 않는 이유는 [GOVERNANCE.md](GOVERNANCE.md) 와
@@ -77,36 +77,11 @@ import {
77
77
  const VOTES_BRANCH = 'axmap/votes'
78
78
  const VOTES_REL = path.join('.axmap', 'votes')
79
79
 
80
- /** 이 파일이 놓인 자리. 팀 저장소에서는 `ci/axmap/governance/` 다. */
80
+ /** 이 파일이 놓인 자리. 옆의 gate.mjs 를 부를 때 쓴다. */
81
81
  const HERE = path.dirname(fileURLToPath(import.meta.url))
82
82
 
83
- /**
84
- * 사본이 놓이는 자리. **저장소 루트 기준**이고 `tools/vendor.mjs` 의 `VENDOR_REL`
85
- * 과 같은 값이어야 한다. 여기가 갈리면 표에 적히는 출처가 조용히 비게 된다.
86
- */
87
- const VENDOR_REL = 'ci/axmap'
88
-
89
- /**
90
- * 벤더링(**남의 저장소 코드를 내 저장소 안에 복사해 두는 것**) 출처가 적힌 파일을
91
- * 찾는 순서.
92
- *
93
- * 🔴 **저장소 루트 쪽을 먼저 본다.** 이 칸이 말하는 것은 "어느 스크립트가 돌았나"
94
- * 가 아니라 **"표를 던지는 이 저장소가 어느 사본을 쓰고 있나"** 이기 때문이다.
95
- * 개발 중에 axMap 저장소의 스크립트를 팀 저장소에서 직접 돌리더라도, 표에는
96
- * 그 팀 저장소의 사본 출처가 적혀야 한다.
97
- *
98
- * 둘 다 없으면 `{ commit: null, behind: null }` — axMap 저장소 자신에서 돌릴 때가
99
- * 그렇다. 사본이 아니니 출처도 없다.
100
- */
101
- const axmapSourceCandidates = () => [
102
- path.join(ROOT ?? '.', ...VENDOR_REL.split('/'), 'SOURCE.json'),
103
- path.join(HERE, '..', 'SOURCE.json'),
104
- ]
105
-
106
83
  /** 게이트를 미리 한 번 돌려 보는 데 주는 시간. 넘기면 "잴 수 없음" 으로 적는다. */
107
84
  const GATE_TIMEOUT_MS = 60_000
108
- /** 원격에 axMap 이 몇 커밋 앞서 있는지 물어보는 데 주는 시간. */
109
- const LS_REMOTE_TIMEOUT_MS = 15_000
110
85
 
111
86
  /** push 가 거부될 때(= 누가 먼저 도착) 다시 해보는 횟수. bin/axmap.mjs 와 같은 값. */
112
87
  const MAX_CAS_RETRIES = 5
@@ -240,7 +215,7 @@ const HELP = `표를 던진다 — 저장소 안의 파일 하나로
240
215
  다만 판정문에는 반드시 보인다.
241
216
 
242
217
  표에는 던질 때 실제로 보였던 것(agent.shown)이 함께 적힌다 — 변경량·경로·
243
- 그 시점의 판정·axMap 사본이 몇 커밋 뒤처졌는지. 🔴 이 칸은 아무도 못 적는다.
218
+ 그 시점의 판정. 🔴 이 칸은 아무도 못 적는다.
244
219
  이 프로그램이 직접 재서 채운다.
245
220
  `
246
221
 
@@ -687,44 +662,6 @@ function measureGate(branch, targetName, policyFlag) {
687
662
  return `${verdict.ok ? '충족' : '미달'} ${verdict.approvals}/${verdict.threshold}`
688
663
  }
689
664
 
690
- /**
691
- * 이 저장소에 복사돼 있는 axMap 이 **어느 커밋의 사본이고 몇 커밋 뒤처졌는가.**
692
- *
693
- * 🔴 못 닿으면 `behind: null` 이고 **표는 그대로 던져진다.** axMap 이 잠깐
694
- * 죽거나 접근 권한이 만료된 날 팀의 투표가 멈추면 안 된다 — 사본을 두는
695
- * 이유(벤더링)와 정확히 같은 이유다.
696
- *
697
- * 🔴 **여기서 fetch 하지 않는다.** 표를 던지는 명령이 남의 저장소를 통째로
698
- * 내려받아 이 저장소의 오브젝트를 불리는 것은 아무도 예상하지 않는 부수효과다.
699
- * 그래서 `ls-remote` 로 끝을 물어보고, 셀 수 있을 때만(= 그 이력이 이미 이
700
- * 저장소에 있을 때만) 센다. 못 세면 `null` 이다.
701
- * ⚠ 그 결과 "원격에 못 닿음" 과 "닿았지만 셀 수 없음" 이 둘 다 `null` 이다.
702
- * `behind: null` 은 **"뒤처지지 않았다" 가 아니라 "못 쟀다"** 로 읽는다.
703
- */
704
- function measureAxmap() {
705
- let src = null
706
- for (const p of axmapSourceCandidates()) {
707
- try { src = JSON.parse(fs.readFileSync(p, 'utf8')); break } catch { /* 다음 자리 */ }
708
- }
709
- if (!src) return { commit: null, behind: null }
710
-
711
- const commit = str(src?.source?.commit)
712
- const url = str(src?.source?.repository)
713
- const branch = str(src?.source?.branch) ?? 'main'
714
- if (!commit) return { commit: null, behind: null }
715
- if (!url) return { commit, behind: null }
716
-
717
- const ls = git(['ls-remote', url, branch], { env: NO_PROMPT_ENV, timeout: LS_REMOTE_TIMEOUT_MS })
718
- if (ls.code !== 0 || !ls.out) return { commit, behind: null }
719
- const tip = ls.out.split('\n')[0].split('\t')[0].trim()
720
- if (!/^[0-9a-f]{40}$/.test(tip)) return { commit, behind: null }
721
- if (tip === commit) return { commit, behind: 0 }
722
-
723
- const n = git(['rev-list', '--count', `${commit}..${tip}`])
724
- if (n.code !== 0 || !/^\d+$/.test(n.out)) return { commit, behind: null }
725
- return { commit, behind: Number(n.out) }
726
- }
727
-
728
665
  // ---------------------------------------------------------------------------
729
666
  // 본체
730
667
  // ---------------------------------------------------------------------------
@@ -886,7 +823,8 @@ const relPath = `votes/${branch}/${voter}-${sha.slice(0, 8)}.json`
886
823
  const shown = {
887
824
  ...measureDiff(targetName, branch),
888
825
  gate: measureGate(branch, targetName, str(flags.policy)),
889
- axmap: measureAxmap(),
826
+ // 🔴 예전에는 `axmap` 칸(저장소 안 사본의 출처 커밋·뒤처진 정도)도 적었다.
827
+ // 사본을 걷어낸 뒤(D31)로는 잴 것이 없다. 옛 표에 남은 그 칸은 판정에 안 쓰인다.
890
828
  }
891
829
 
892
830
  // 시각(`at`)은 위에서 한 번 정했다. CAS 재시도마다 바뀌면 같은 표가 매번 다른
@@ -940,8 +878,7 @@ for (let attempt = 1; attempt <= MAX_CAS_RETRIES; attempt++) {
940
878
  // 무엇이 적혔는지 모르고, 모르는 칸은 아무도 안 읽는다.
941
879
  console.log(
942
880
  ` 본 것: ${shown.files === null ? '못 쟀음' : `${shown.files}파일 +${shown.insertions}/-${shown.deletions}`}`
943
- + ` · 던지기 직전 판정: ${shown.gate}`
944
- + ` · axMap 사본: ${shown.axmap.behind === null ? '뒤처짐 못 쟀음' : `${shown.axmap.behind}커밋 뒤`}`,
881
+ + ` · 던지기 직전 판정: ${shown.gate}`,
945
882
  )
946
883
  if (voteValue === 'reject') {
947
884
  console.log(' reject 는 정족수 계산에 들어가지 않습니다 — 판정문에 보이기만 합니다.')
package/mcp/README.md CHANGED
@@ -148,6 +148,8 @@ CLI 를 부를 때마다 넘긴다. 앞 두 칸은 CLI 와 순서가 같아서,
148
148
 
149
149
  서버는 도구를 부를 때마다 대상 저장소를 다시 정한다. 순서는 `AXMAP_REPO` → 도구의 절대 경로 `cwd` → 클라이언트 작업 폴더(roots) → 시작 폴더의 Git 루트다. 근거는 [DECISIONS.md의 D22](../docs/DECISIONS.md).
150
150
 
151
+ 그 폴더가 Git 저장소가 아니면(예: 한 저장소의 worktree 여러 개를 담은 작업 폴더) `.axmap.json`의 `repo` 수동 설정 → 바로 아래 폴더의 저장소 자동 탐색(같은 저장소의 worktree뿐이면 메인 체크아웃) 순으로 정한다. 서로 다른 저장소가 둘 이상이면 고르지 않고 후보를 오류로 알린다. 도구의 `cwd`를 주거나 그 폴더에서 `axmap workspace use <저장소 폴더>`로 `.axmap.json`을 쓰면 서버를 다시 켜지 않아도 다음 호출부터 따른다. `axmap workspace show`는 지금 무엇을 왜 고르는지 보여 준다. 근거는 D30.
152
+
151
153
  장부가 없는 repo도 첫 조회·선점 때 자동으로 준비한다. 기존 원격 장부가 있으면 이어 사용하고, 원격 접근 실패는 빈 장부로 바꾸지 않는다. roots가 여러 repo를 가리켜 대상을 고를 수 없으면 도구의 `cwd`를 지정한다. roots 미지원 클라이언트에서 실행 중 폴더를 바꿀 때도 `cwd`를 전달한다. 서로 다른 MCP 세션의 타겟은 독립적이다.
152
154
 
153
155
  전역 등록인데 `AXMAP_REPO` 에 한 폴더를 박아 두면 **등록은 전역인데 대상은 한 곳으로