coxpit 6.0.0 → 6.1.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.
- package/package.json +1 -1
- package/src/agentstate.ts +35 -3
- package/src/board.ts +14 -8
- package/src/cockpit.ts +744 -26
- package/src/orchestrator.ts +113 -11
- package/src/procscan.ts +241 -0
- package/src/server.ts +89 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "Self-hosted cockpit for running a fleet of AI coding agents across your own machines — parallel worktree runs, live board, compare & merge, web terminal, design capture.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/agentstate.ts
CHANGED
|
@@ -22,7 +22,7 @@ const HOOK_COOLDOWN_MS = 60_000; // run 하나가 웹훅을 때릴 수 있는
|
|
|
22
22
|
|
|
23
23
|
interface Pattern { id: string; re: RegExp }
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// [!] 휴리스틱이고 provider TUI 화면에 종속적이다. "에이전트의 의도"를 안다고 주장하지 않는다 —
|
|
26
26
|
// 말하는 것은 "화면에 입력/승인 프롬프트가 떠 있다" 하나뿐이다. CLI 가 화면을 바꾸면 이 표만
|
|
27
27
|
// 고치고, provider 를 늘리는 일은 행을 늘리는 일이다. 확신이 없으면 넣지 않는다:
|
|
28
28
|
// 빗나간 idle 은 받아들일 수 있고, 지어낸 waiting 은 받아들일 수 없다.
|
|
@@ -90,13 +90,38 @@ interface Tracker {
|
|
|
90
90
|
lastByteAt: number;
|
|
91
91
|
timer: ReturnType<typeof setTimeout> | null; // tracker 당 정확히 하나
|
|
92
92
|
lastHookAt: number; // 웹훅 쿨다운 — tracker 와 함께 살고 함께 죽는다
|
|
93
|
+
spotted: Set<number>; // B3 수동 포트 감지 — 출력에서 본 LISTEN 포트(감지만, 행동 없음)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// B3: 출력에서 "listening on :PORT" / "http(s)://host:PORT" 만 보수적으로 집는다.
|
|
97
|
+
// A2 와 같은 규율 — 못 맞히면 빈손이고, 포트를 지어내지 않는다. 1..65535 만 인정.
|
|
98
|
+
const PORT_PATTERNS: RegExp[] = [
|
|
99
|
+
/\blisten(?:ing)?\b[^0-9]{0,20}:(\d{2,5})\b/gi, // "Listening on :3000", "listening at port 8080" 근처의 :NNNN
|
|
100
|
+
/\bhttps?:\/\/[a-z0-9.\-]+:(\d{2,5})\b/gi, // http://localhost:3000
|
|
101
|
+
/\b(?:0\.0\.0\.0|127\.0\.0\.1|localhost)\b[^0-9]{0,4}:(\d{2,5})\b/gi,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
function spotPorts(t: Tracker, chunk: string): void {
|
|
105
|
+
for (const re of PORT_PATTERNS) {
|
|
106
|
+
re.lastIndex = 0;
|
|
107
|
+
let m: RegExpExecArray | null;
|
|
108
|
+
while ((m = re.exec(chunk))) {
|
|
109
|
+
const p = Number(m[1]);
|
|
110
|
+
if (p >= 1 && p <= 65535) t.spotted.add(p);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// 무한정 쌓지 않는다 — 최근 것 위주로 상한
|
|
114
|
+
if (t.spotted.size > 24) {
|
|
115
|
+
const keep = [...t.spotted].slice(-24);
|
|
116
|
+
t.spotted = new Set(keep);
|
|
117
|
+
}
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
/**
|
|
96
121
|
* 주의 환기의 서버 쪽 절반(spec v5.28 A5) — 코크핏이 아예 닫혀 있을 때 유일하게 남는 신호다.
|
|
97
122
|
* orchestrator 의 notifySettle 과 같은 모양으로 POST 하고, 실패는 무해하게 삼킨다.
|
|
98
123
|
*
|
|
99
|
-
*
|
|
124
|
+
* [!] **상태만 보낸다.** detail 도, tail 조각도 절대 태우지 않는다 — 터미널 출력은 시크릿을
|
|
100
125
|
* 그대로 뱉을 수 있고 웹훅 엔드포인트는 coxpit 의 신뢰 경계 **밖**이다(꼬리는 인증된 /ws 허브에만).
|
|
101
126
|
*/
|
|
102
127
|
async function postHook(runId: number, state: AgentState): Promise<void> {
|
|
@@ -173,7 +198,7 @@ function onQuiet(runId: number): void {
|
|
|
173
198
|
export function attach(runId: number): void {
|
|
174
199
|
const cur = trackers.get(runId);
|
|
175
200
|
if (cur) { cur.refs++; return; }
|
|
176
|
-
trackers.set(runId, { refs: 1, state: 'unknown', since: Date.now(), tail: '', lastByteAt: 0, timer: null, lastHookAt: 0 });
|
|
201
|
+
trackers.set(runId, { refs: 1, state: 'unknown', since: Date.now(), tail: '', lastByteAt: 0, timer: null, lastHookAt: 0, spotted: new Set() });
|
|
177
202
|
}
|
|
178
203
|
|
|
179
204
|
/** 출력 청크 — tail 에 붙이고 시각을 찍고 working. 미러 중복이 들어와도 해롭지 않다. */
|
|
@@ -183,10 +208,17 @@ export function feed(runId: number, chunk: string): void {
|
|
|
183
208
|
t.tail += chunk;
|
|
184
209
|
if (t.tail.length > TAIL_MAX) t.tail = t.tail.slice(t.tail.length - TAIL_MAX);
|
|
185
210
|
t.lastByteAt = Date.now();
|
|
211
|
+
spotPorts(t, chunk); // B3 수동 포트 감지 — 감지만, 행동 없음
|
|
186
212
|
setState(runId, t, 'working');
|
|
187
213
|
arm(runId, t, ACTIVE_MS);
|
|
188
214
|
}
|
|
189
215
|
|
|
216
|
+
/** B3: 이 run 의 출력에서 감지한 LISTEN 포트 목록(감지만; 코크핏이 원클릭 대상으로 제안). */
|
|
217
|
+
export function spottedPorts(runId: number): number[] {
|
|
218
|
+
const t = trackers.get(runId);
|
|
219
|
+
return t ? [...t.spotted] : [];
|
|
220
|
+
}
|
|
221
|
+
|
|
190
222
|
/**
|
|
191
223
|
* 사람이 터미널에 입력했다 — waiting 을 즉시 지운다.
|
|
192
224
|
* TUI 는 다시 그리므로 이미 답한 프롬프트 문구가 tail 에 남는다. 입력 신호는 "사람이 응답했다"는
|
package/src/board.ts
CHANGED
|
@@ -1928,23 +1928,29 @@ $('archList').addEventListener('click', async (e)=>{
|
|
|
1928
1928
|
}catch{ toast('could not open task', 'error'); }
|
|
1929
1929
|
});
|
|
1930
1930
|
|
|
1931
|
-
/* ── Reclaim orphaned worktrees ──
|
|
1932
|
-
(~180MB each — node_modules lives inside).
|
|
1933
|
-
|
|
1931
|
+
/* ── Reclaim orphaned worktrees ── finished run worktrees are disk debt
|
|
1932
|
+
(~180MB each — node_modules lives inside). Active work is never listed by the
|
|
1933
|
+
server. v6.0 T6b: settled done worktrees are listed too, but any whose change
|
|
1934
|
+
set is unmerged AND un-exported is flagged (reclaimRisk) — this button reclaims
|
|
1935
|
+
only the safe ones, so the count here is the SAFE count. Pick a flagged one
|
|
1936
|
+
deliberately in the cockpit's worktree sheet. */
|
|
1934
1937
|
let reclaimN = 0;
|
|
1935
1938
|
async function reclaimRefresh(){
|
|
1936
1939
|
try{
|
|
1937
1940
|
const j = await fetch('/api/worktrees').then(x=>x.json());
|
|
1938
|
-
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
+
const items = (j.items||[]);
|
|
1942
|
+
const safe = items.filter(w=>!w.reclaimRisk);
|
|
1943
|
+
reclaimN = safe.length;
|
|
1944
|
+
const mb = Math.round(safe.reduce((s,w)=>s+(w.sizeKb||0),0)/1024);
|
|
1945
|
+
const flagged = items.length - safe.length;
|
|
1946
|
+
$('reclaimHint').textContent = reclaimN ? (reclaimN+' · ~'+mb+'MB'+(flagged?(' (+'+flagged+' flagged)'):'')) : '';
|
|
1941
1947
|
$('reclaimBtn').hidden = reclaimN===0;
|
|
1942
1948
|
}catch{ $('reclaimBtn').hidden = true; }
|
|
1943
1949
|
}
|
|
1944
1950
|
$('reclaimBtn').addEventListener('click', async ()=>{
|
|
1945
1951
|
if (!reclaimN) return;
|
|
1946
|
-
const ok = await confirmUI('remove '+reclaimN+'
|
|
1947
|
-
sub:'active work is untouched —
|
|
1952
|
+
const ok = await confirmUI('remove '+reclaimN+' finished run worktree'+(reclaimN===1?'':'s')+'?', {
|
|
1953
|
+
sub:'active work is untouched, and worktrees holding unmerged, un-exported changes are left alone — tick those one by one in the cockpit',
|
|
1948
1954
|
okLabel:'Reclaim', danger:true });
|
|
1949
1955
|
if (!ok) return;
|
|
1950
1956
|
try{
|