axmap-cli 0.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/.claude/commands/ax-done.md +10 -0
- package/.claude/commands/ax-setup.md +31 -0
- package/.claude/commands/ax-start.md +15 -0
- package/.claude/commands/ax-tell.md +14 -0
- package/.claude/commands/ax-update.md +19 -0
- package/.claude/commands/ax.md +13 -0
- package/CLAUDE.md +309 -0
- package/LICENSE +20 -0
- package/README.md +207 -0
- package/app/README.md +366 -0
- package/app/eval/edges.mjs +242 -0
- package/app/lib/adjacent.mjs +125 -0
- package/app/lib/agentcli.mjs +153 -0
- package/app/lib/analyze.mjs +1159 -0
- package/app/lib/cochange.mjs +421 -0
- package/app/lib/datanodes.mjs +127 -0
- package/app/lib/entry.mjs +192 -0
- package/app/lib/featuregraph.mjs +389 -0
- package/app/lib/features.mjs +645 -0
- package/app/lib/fetchrepo-run.mjs +37 -0
- package/app/lib/fetchrepo.mjs +164 -0
- package/app/lib/flow.mjs +1089 -0
- package/app/lib/ladder.mjs +387 -0
- package/app/lib/langs.mjs +630 -0
- package/app/lib/live.mjs +346 -0
- package/app/lib/llm.mjs +594 -0
- package/app/lib/newfile.mjs +126 -0
- package/app/lib/prdiff.mjs +651 -0
- package/app/lib/reveal.mjs +316 -0
- package/app/lib/roots.mjs +186 -0
- package/app/lib/scope.mjs +342 -0
- package/app/lib/session.mjs +389 -0
- package/app/lib/slots.mjs +233 -0
- package/app/lib/ssot.mjs +277 -0
- package/app/lib/teamview.mjs +962 -0
- package/app/lib/terms.ko.mjs +169 -0
- package/app/server.mjs +1959 -0
- package/app/web/shell.css +538 -0
- package/app/web/shell.html +197 -0
- package/app/web/shell.js +638 -0
- package/app/web/stage.js +347 -0
- package/app/web/words.js +85 -0
- package/bin/axmap.mjs +1918 -0
- package/governance/GOVERNANCE.md +433 -0
- package/governance/gate.mjs +526 -0
- package/governance/vote.mjs +501 -0
- package/mcp/README.md +254 -0
- package/mcp/SETUP-FOR-AI.md +186 -0
- package/mcp/install.ps1 +341 -0
- package/mcp/install.sh +339 -0
- package/mcp/server.mjs +969 -0
- package/package.json +48 -0
- package/src/closure.mjs +343 -0
- package/src/governance.mjs +839 -0
- package/src/invariants.mjs +226 -0
- package/src/mrtarget.mjs +284 -0
- package/src/promote.mjs +177 -0
- package/src/protocol.mjs +423 -0
- package/src/repotarget.mjs +81 -0
- package/src/update.mjs +177 -0
- package/src/version.mjs +186 -0
- package/tools/bus.mjs +520 -0
- package/tools/cluster-experiment.mjs +256 -0
- package/tools/cluster-sweep.mjs +226 -0
- package/tools/make-icon.mjs +108 -0
- package/tools/mcp-register.mjs +269 -0
- package/tools/mr-target.mjs +49 -0
- package/tools/persona-bench.mjs +362 -0
- package/tools/pick-repo.mjs +229 -0
- package/tools/promote.mjs +550 -0
- package/tools/reveal-demo.mjs +158 -0
- package/tools/run-tests.mjs +42 -0
- package/tools/setup.mjs +490 -0
- package/tools/shortcut.mjs +121 -0
- package/tools/smoke.mjs +166 -0
- package/tools/topicgraph.py +154 -0
- package/tools/vendor.mjs +382 -0
- package/tools/version.mjs +115 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 기능 인접 경고 — 파일은 안 겹치는데 **기능이 겹칠 때**.
|
|
3
|
+
*
|
|
4
|
+
* v0 프로토콜은 경로가 겹치는지만 본다. 그것으로 못 잡는 상황이 있다.
|
|
5
|
+
*
|
|
6
|
+
* 에이전트 A 가 `album.controller.ts` 를 잡는다 (API 쪽, 앞에서부터)
|
|
7
|
+
* 에이전트 B 가 `album.repository.ts` 를 잡는다 (DB 쪽, 뒤에서부터)
|
|
8
|
+
*
|
|
9
|
+
* 경로가 안 겹치므로 **둘 다 통과한다.** 그런데 같은 기능이다.
|
|
10
|
+
* A 가 응답 형태를 바꾸고 B 가 스키마를 바꾸면, 머지 시점에 텍스트 충돌은
|
|
11
|
+
* 하나도 없는데 기능이 깨진다. 이것이 파일 단위 락의 한계다.
|
|
12
|
+
*
|
|
13
|
+
* 🔴 이것은 **경고이지 거부가 아니다.** 프로토콜을 건드리지 않는다.
|
|
14
|
+
*
|
|
15
|
+
* checkOverlap 은 그대로 두고, 뷰어가 옆에서 알려주기만 한다. 이유가 있다 —
|
|
16
|
+
*
|
|
17
|
+
* 1. 거부로 만들면 통과가 **줄어드는** 게 아니라 판정 기준이 달라진다.
|
|
18
|
+
* "경로가 겹치면 거부" 는 결정론이고 증명 가능하지만(I1), "기능이 겹치면
|
|
19
|
+
* 거부" 는 lift 문턱이라는 조절 가능한 숫자에 기댄다. 그 숫자를 정당화할
|
|
20
|
+
* 근거가 아직 없다 (DECISIONS.md 열린 질문 Q5).
|
|
21
|
+
* 2. 히스토리가 없는 새 저장소에서는 이 신호가 아예 없다. 락이 그런 것에
|
|
22
|
+
* 의존하면 "어떤 저장소에서는 락이 약해진다" 는 상태가 된다.
|
|
23
|
+
*
|
|
24
|
+
* 그래서 게이트는 그대로, 사람에게 정보만 준다 — D4 가 LLM 을 게이트 밖에
|
|
25
|
+
* 둔 것과 같은 배치다.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** 경로 접두사 포함. protocol.mjs 의 겹침 판정과 같은 '/' 경계 규칙을 쓴다. */
|
|
29
|
+
const covers = (claimPath, file) => file === claimPath || file.startsWith(`${claimPath}/`)
|
|
30
|
+
|
|
31
|
+
/** 이 claim 이 덮는 실제 파일들 */
|
|
32
|
+
function filesOf(claim, allPaths) {
|
|
33
|
+
const out = new Set()
|
|
34
|
+
for (const c of claim.paths ?? []) {
|
|
35
|
+
for (const f of allPaths) if (covers(c, f)) out.add(f)
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 활성 claim 들 사이에서 "경로는 안 겹치는데 공변경으로 이어진" 쌍을 찾는다.
|
|
42
|
+
*
|
|
43
|
+
* @param {object[]} claims 활성 claim (agent, paths, task, intent…)
|
|
44
|
+
* @param {object[]} coEdges 공변경 엣지 (source, target, support, lift)
|
|
45
|
+
* @param {string[]} allPaths 그래프에 있는 파일 전체
|
|
46
|
+
* @param {object} opts
|
|
47
|
+
* @returns {{pairs: object[], byAgent: object}}
|
|
48
|
+
*/
|
|
49
|
+
export function featureAdjacency(claims, coEdges, allPaths, {
|
|
50
|
+
minLift = 3, minSupport = 4, maxEvidence = 8,
|
|
51
|
+
} = {}) {
|
|
52
|
+
const live = (claims ?? []).filter((c) => !c.broken && (c.paths?.length ?? 0) > 0)
|
|
53
|
+
if (live.length < 2) return { pairs: [], checked: live.length }
|
|
54
|
+
|
|
55
|
+
const owned = live.map((c) => ({ claim: c, files: filesOf(c, allPaths) }))
|
|
56
|
+
|
|
57
|
+
// 파일 → 그 파일을 덮는 claim 들. 같은 파일을 둘이 덮고 있으면 그건
|
|
58
|
+
// 프로토콜이 이미 막았어야 하는 상황이므로 여기서 다루지 않는다.
|
|
59
|
+
const ownerOf = new Map()
|
|
60
|
+
owned.forEach((o, i) => { for (const f of o.files) ownerOf.set(f, i) })
|
|
61
|
+
|
|
62
|
+
const pairKey = (i, j) => (i < j ? `${i}\0${j}` : `${j}\0${i}`)
|
|
63
|
+
const acc = new Map()
|
|
64
|
+
|
|
65
|
+
for (const e of coEdges) {
|
|
66
|
+
if ((e.lift ?? 0) < minLift || (e.support ?? 0) < minSupport) continue
|
|
67
|
+
const a = ownerOf.get(e.source)
|
|
68
|
+
const b = ownerOf.get(e.target)
|
|
69
|
+
if (a === undefined || b === undefined || a === b) continue
|
|
70
|
+
|
|
71
|
+
const k = pairKey(a, b)
|
|
72
|
+
if (!acc.has(k)) acc.set(k, { a: Math.min(a, b), b: Math.max(a, b), evidence: [] })
|
|
73
|
+
acc.get(k).evidence.push({
|
|
74
|
+
from: e.source, to: e.target, lift: e.lift, support: e.support,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const pairs = [...acc.values()].map((p) => {
|
|
79
|
+
const A = owned[p.a].claim
|
|
80
|
+
const B = owned[p.b].claim
|
|
81
|
+
const ev = p.evidence.sort((x, y) => y.lift - x.lift).slice(0, maxEvidence)
|
|
82
|
+
return {
|
|
83
|
+
agents: [A.agent, B.agent],
|
|
84
|
+
tasks: [A.task ?? null, B.task ?? null],
|
|
85
|
+
intents: [A.intent ?? null, B.intent ?? null],
|
|
86
|
+
paths: [A.paths, B.paths],
|
|
87
|
+
// 세기는 "몇 개의 공변경 엣지가 두 영역을 가로지르나" + 가장 강한 lift.
|
|
88
|
+
// 하나의 점수로 합치지 않는다 — 둘은 다른 것을 뜻한다.
|
|
89
|
+
// (엣지 수 = 얼마나 넓게 얽혔나 / 최고 lift = 얼마나 강하게 얽혔나)
|
|
90
|
+
crossings: p.evidence.length,
|
|
91
|
+
topLift: ev[0]?.lift ?? 0,
|
|
92
|
+
evidence: ev,
|
|
93
|
+
}
|
|
94
|
+
}).sort((x, y) => y.crossings - x.crossings || y.topLift - x.topLift)
|
|
95
|
+
|
|
96
|
+
return { pairs, checked: live.length }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 사람이 읽을 경고문.
|
|
101
|
+
*
|
|
102
|
+
* 거부 메시지와 **같은 모양**으로 만든다. 이 저장소의 거부 메시지는
|
|
103
|
+
* "누가·무엇을·언제까지" 를 말해서 에이전트가 스스로 방향을 틀 수 있게 한다.
|
|
104
|
+
* 경고도 같은 재료를 줘야 같은 판단을 할 수 있다.
|
|
105
|
+
*/
|
|
106
|
+
export function formatAdjacency(pair) {
|
|
107
|
+
const [a, b] = pair.agents
|
|
108
|
+
const lines = [
|
|
109
|
+
`⚠ 경로는 안 겹치지만 같은 기능일 수 있습니다`,
|
|
110
|
+
``,
|
|
111
|
+
` ${a} (${pair.tasks[0] ?? '작업 미지정'})`,
|
|
112
|
+
` ${pair.paths[0].join(', ')}`,
|
|
113
|
+
pair.intents[0] ? ` 의도: ${pair.intents[0]}` : null,
|
|
114
|
+
``,
|
|
115
|
+
` ${b} (${pair.tasks[1] ?? '작업 미지정'})`,
|
|
116
|
+
` ${pair.paths[1].join(', ')}`,
|
|
117
|
+
pair.intents[1] ? ` 의도: ${pair.intents[1]}` : null,
|
|
118
|
+
``,
|
|
119
|
+
` 근거 — 두 영역을 가로지르는 공변경 ${pair.crossings}건 (최고 lift ${pair.topLift})`,
|
|
120
|
+
...pair.evidence.slice(0, 4).map((e) => ` ${e.from} ↔ ${e.to} lift ${e.lift} · n=${e.support}`),
|
|
121
|
+
``,
|
|
122
|
+
` 이것은 거부가 아닙니다. 두 사람이 서로의 의도를 아는 것으로 충분할 수 있습니다.`,
|
|
123
|
+
]
|
|
124
|
+
return lines.filter((l) => l !== null).join('\n')
|
|
125
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 어떤 AI CLI 가 이 PC 에 있고, 로그인돼 있는가.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 왜 필요한가.
|
|
5
|
+
*
|
|
6
|
+
* 화면에서는 "새 대화" 지만 실제로는 **CLI 프로세스를 하나 띄우는 것**이다.
|
|
7
|
+
* 그 CLI 에 로그인이 안 돼 있으면 프롬프트를 보낸 뒤에야 로그인 화면이 뜨는데,
|
|
8
|
+
* 우리는 터미널을 감춰 놨으므로 사용자에게는 그냥 **아무 일도 안 일어난 것**으로
|
|
9
|
+
* 보인다. 그래서 대화를 시작하기 전에 먼저 물어본다.
|
|
10
|
+
*
|
|
11
|
+
* 세 CLI 모두 같은 모양의 비대화형 모드를 갖고 있어서 한 겹으로 덮을 수 있다.
|
|
12
|
+
*
|
|
13
|
+
* -p / --print 한 번 묻고 끝낸다
|
|
14
|
+
* --output-format stream-json JSONL 로 흘려준다 → 채팅 말풍선으로 다시 그린다
|
|
15
|
+
* --resume / --conversation <id> 앞 대화에 이어붙인다
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'node:child_process'
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import os from 'node:os'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
|
|
23
|
+
const HOME = os.homedir()
|
|
24
|
+
const hasHome = (rel) => { try { return fs.existsSync(path.join(HOME, rel)) } catch { return false } }
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 🔴 `claude` 는 세션 id 를 **받고**, `agy` 는 **주기만 한다.**
|
|
28
|
+
*
|
|
29
|
+
* 그래서 이어붙이는 방법이 둘로 갈린다.
|
|
30
|
+
* 받는 쪽 — 우리가 uuid 를 만들어 첫 턴에 박고, 그 값으로 resume 한다
|
|
31
|
+
* 주는 쪽 — 첫 턴 응답에서 id 를 주워 두었다가 다음 턴에 쓴다
|
|
32
|
+
* 둘 다 `pickId()` 로 응답에서 id 를 줍는다. 받는 쪽도 주워 두면 우리가 만든
|
|
33
|
+
* 값과 CLI 가 실제로 쓴 값이 어긋났을 때 CLI 쪽을 따라갈 수 있다.
|
|
34
|
+
*/
|
|
35
|
+
export const AGENTS = [
|
|
36
|
+
{
|
|
37
|
+
id: 'claude',
|
|
38
|
+
name: 'Claude Code',
|
|
39
|
+
bin: 'claude',
|
|
40
|
+
creds: ['.claude/.credentials.json', '.claude.json'],
|
|
41
|
+
takesId: true,
|
|
42
|
+
start: (id) => ['-p', '--output-format', 'stream-json', '--verbose', '--session-id', id],
|
|
43
|
+
resume: (cid) => ['-p', '--output-format', 'stream-json', '--verbose', '--resume', cid],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'agy',
|
|
47
|
+
name: 'agy',
|
|
48
|
+
bin: 'agy',
|
|
49
|
+
/**
|
|
50
|
+
* 🔴 여기에 `AppData/Local/agy` 를 넣었다가 뺐다.
|
|
51
|
+
*
|
|
52
|
+
* 그건 **실행 파일이 있는 자리**이지 자격증명이 아니다. 넣어 두면 설치만
|
|
53
|
+
* 해도 "로그인됨" 으로 뜬다 — 확신 있게 틀린 답이고, 이 저장소가 가장
|
|
54
|
+
* 싫어하는 종류의 실패다(D5). 모르면 `unknown` 이 맞는 답이다.
|
|
55
|
+
*/
|
|
56
|
+
creds: ['.agy/auth.json', '.agy/credentials.json', '.config/agy/auth.json'],
|
|
57
|
+
takesId: false,
|
|
58
|
+
start: () => ['-p', '--output-format', 'stream-json'],
|
|
59
|
+
resume: (cid) => ['-p', '--output-format', 'stream-json', '--conversation', cid],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'codex',
|
|
63
|
+
name: 'Codex CLI',
|
|
64
|
+
bin: 'codex',
|
|
65
|
+
creds: ['.codex/auth.json'],
|
|
66
|
+
takesId: false,
|
|
67
|
+
start: () => ['exec', '--json', '-'],
|
|
68
|
+
resume: (cid) => ['exec', 'resume', cid, '--json', '-'],
|
|
69
|
+
/**
|
|
70
|
+
* 🔴 이 PC 에 codex 가 없어서 **실물로 확인하지 못했다.**
|
|
71
|
+
* 확인 안 된 것을 확인된 것처럼 화면에 내보내지 않는다 — 화면이 이 표시를
|
|
72
|
+
* 그대로 띄운다. 실제 codex 가 있는 PC 에서 한 번 돌려보고 이 줄을 지운다.
|
|
73
|
+
*/
|
|
74
|
+
unverified: true,
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
export const agentById = (id) => AGENTS.find((a) => a.id === id) ?? null
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 실행 파일의 실제 경로.
|
|
82
|
+
*
|
|
83
|
+
* 🔴 윈도우의 `spawn('claude', …)` 은 확장자 없는 이름을 못 찾는다.
|
|
84
|
+
* `shell: true` 로 우회하면 프롬프트 안의 따옴표가 셸에 먹혀 **조용히 잘린다** —
|
|
85
|
+
* 사용자가 쓴 말이 반쯤 사라진 채로 AI 에게 간다는 뜻이라 최악이다.
|
|
86
|
+
* 그래서 경로를 먼저 풀어서 그 경로로 직접 띄운다. 프롬프트는 stdin 으로 준다.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveBin(bin) {
|
|
89
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which'
|
|
90
|
+
try {
|
|
91
|
+
const r = spawnSync(cmd, [bin], { encoding: 'utf8', windowsHide: true })
|
|
92
|
+
if (r.status !== 0) return null
|
|
93
|
+
return r.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] ?? null
|
|
94
|
+
} catch { return null }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 로그인 여부는 **모르면 모른다고 한다.**
|
|
99
|
+
*
|
|
100
|
+
* 자격증명이 있는 자리는 CLI 버전마다 바뀌고, macOS 는 키체인에 넣어서 파일이
|
|
101
|
+
* 아예 없다. 없다고 단정하면 멀쩡한 CLI 를 못 쓰게 막고, 있다고 단정하면 첫
|
|
102
|
+
* 프롬프트에서 로그인 화면이 떠 대화가 그냥 멈춘다. 둘 다 조용한 실패다.
|
|
103
|
+
* 그래서 `yes` · `no` · `unknown` 셋으로 답하고, 화면이 그 셋을 다르게 그린다.
|
|
104
|
+
*
|
|
105
|
+
* 🔴 그러므로 `unknown` 을 "아마 되겠지" 로 접지 마라. 그 값이 화면에
|
|
106
|
+
* "로그인 상태를 확인하지 못했습니다" 로 그대로 나가는 것이 설계다 —
|
|
107
|
+
* 터미널을 감춰 놨기 때문에 침묵이 곧 미궁이 된다.
|
|
108
|
+
*/
|
|
109
|
+
function signedIn(a, installed) {
|
|
110
|
+
if (!installed) return 'no'
|
|
111
|
+
return a.creds.some(hasHome) ? 'yes' : 'unknown'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 이 PC 에서 쓸 수 있는 CLI 목록. 화면이 시작할 때 한 번 묻는다. */
|
|
115
|
+
export function detect() {
|
|
116
|
+
return AGENTS.map((a) => {
|
|
117
|
+
const bin = resolveBin(a.bin)
|
|
118
|
+
return {
|
|
119
|
+
id: a.id,
|
|
120
|
+
name: a.name,
|
|
121
|
+
installed: !!bin,
|
|
122
|
+
path: bin,
|
|
123
|
+
signedIn: signedIn(a, !!bin),
|
|
124
|
+
unverified: !!a.unverified,
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 설치돼 있는 것 중 첫 번째. 화면이 기본값으로 쓴다. */
|
|
130
|
+
export function preferred(found = detect()) {
|
|
131
|
+
return found.find((a) => a.installed && a.signedIn !== 'no') ?? found.find((a) => a.installed) ?? null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 이번 턴에 띄울 인자.
|
|
136
|
+
* `cid` 가 있으면 이어붙이고, 없으면 새로 시작한다.
|
|
137
|
+
*/
|
|
138
|
+
export function argvFor(agent, { cid, newId }) {
|
|
139
|
+
return cid ? agent.resume(cid) : agent.start(newId)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 응답 이벤트에서 대화 id 를 줍는다.
|
|
144
|
+
* CLI 마다 이름이 달라서 아는 이름을 전부 본다. 못 찾으면 null 이고,
|
|
145
|
+
* 그러면 다음 턴은 **이어붙지 않고 새 대화가 된다** — 조용히 섞이는 것보다 낫다.
|
|
146
|
+
*/
|
|
147
|
+
export function pickId(ev) {
|
|
148
|
+
if (!ev || typeof ev !== 'object') return null
|
|
149
|
+
for (const k of ['session_id', 'sessionId', 'conversation_id', 'conversationId', 'thread_id']) {
|
|
150
|
+
if (typeof ev[k] === 'string' && ev[k]) return ev[k]
|
|
151
|
+
}
|
|
152
|
+
return null
|
|
153
|
+
}
|