agentlas 1.0.21 → 1.0.23
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/CHANGELOG.md +20 -0
- package/README.md +9 -26
- package/engine/agentlas-banner.cjs +0 -1
- package/engine/agentlas-capabilities.cjs +8 -71
- package/engine/agentlas-core-harness.cjs +18 -11
- package/engine/agentlas-experience-exchange.cjs +11 -75
- package/engine/agentlas-i18n.cjs +21 -21
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +4 -31
- package/engine/agentlas-ui.cjs +13 -1
- package/engine/agentlas-workload-routing.cjs +11 -24
- package/engine/agentlas.cjs +3 -13
- package/engine/agents/router.cjs +0 -1
- package/engine/architecture.data.json +5 -5
- package/engine/bootstrap-schema.sql +3 -3
- package/engine/commands/doctor.cjs +3 -3
- package/engine/commands/help.cjs +4 -7
- package/engine/commands/index.cjs +1 -6
- package/engine/experience/runtime.cjs +1 -0
- package/engine/sessions/orchestrator.cjs +4 -21
- package/engine/sessions/session.cjs +66 -5
- package/engine/sessions/sink.cjs +13 -3
- package/engine/sessions/store.cjs +2 -2
- package/engine/ui/palette.cjs +3 -12
- package/engine/ui/renderer.cjs +3 -6
- package/engine/ui/repl.cjs +41 -93
- package/engine/workforce/deps.cjs +9 -6
- package/package.json +4 -3
- package/engine/agentlas-doctor.cjs +0 -224
- package/engine/commands/chat.cjs +0 -12
- package/engine/commands/chats.cjs +0 -33
- package/engine/commands/open.cjs +0 -52
package/engine/ui/renderer.cjs
CHANGED
|
@@ -48,7 +48,6 @@ class Renderer {
|
|
|
48
48
|
}
|
|
49
49
|
const lines = text.trim().split(/\r?\n/).slice(-12);
|
|
50
50
|
for (const l of lines) this.ui.line(" " + l);
|
|
51
|
-
if (session.lastError) this.ui.error(session.lastError);
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
_render(ev) {
|
|
@@ -57,7 +56,6 @@ class Renderer {
|
|
|
57
56
|
case "turn-start": this._beginChrome(); return;
|
|
58
57
|
case "turn-end":
|
|
59
58
|
ui.endTurn();
|
|
60
|
-
if (!ev.ok && ev.error) ui.error(ev.error);
|
|
61
59
|
return;
|
|
62
60
|
case "status": ui.status(ev.text); return;
|
|
63
61
|
case "warn": ui.warn(ev.text); return;
|
|
@@ -65,7 +63,6 @@ class Renderer {
|
|
|
65
63
|
// 사용자가 의도적으로 중단(kill)한 턴의 종료 에러(SIGTERM exit 등)는 소음 —
|
|
66
64
|
// 실사용 테스트에서 확인된 UX 결함. 중단 안내는 REPL이 이미 출력했다.
|
|
67
65
|
if (this.session && this.session.status === "killed") return;
|
|
68
|
-
ui.error(ev.text);
|
|
69
66
|
return;
|
|
70
67
|
case "line": ui.line(ev.text); return;
|
|
71
68
|
case "tool": ui.tool(ev.name, ev.summary); return;
|
|
@@ -104,12 +101,12 @@ class Renderer {
|
|
|
104
101
|
ui.line(ui.c.dim(` ↳ agentlas automation list · agentlas automation off ${String(ev.id || "").slice(0, 8)}`));
|
|
105
102
|
return;
|
|
106
103
|
}
|
|
107
|
-
case "automation-refused":
|
|
104
|
+
case "automation-refused": return;
|
|
108
105
|
case "delegate-spawned":
|
|
109
106
|
ui.ok(`delegate spawned: ${ev.target} → ${ev.key}`);
|
|
110
107
|
return;
|
|
111
|
-
case "delegate-refused":
|
|
112
|
-
case "fence-error":
|
|
108
|
+
case "delegate-refused": return;
|
|
109
|
+
case "fence-error": return;
|
|
113
110
|
case "memory-curated":
|
|
114
111
|
ui.line(ui.c.dim(` ↳ memory: ${ev.written}/${ev.candidates} written (permission: ${ev.permission})`));
|
|
115
112
|
return;
|
package/engine/ui/repl.cjs
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* ui/repl —
|
|
3
|
+
* ui/repl — 프로젝트 Work 컨트롤러와 실행 트리.
|
|
4
4
|
*
|
|
5
5
|
* 원칙:
|
|
6
6
|
* - 실행 경로는 orchestrator 하나. 포그라운드 턴도 세션이다.
|
|
7
7
|
* - 화면은 활성 세션 하나만 스트리밍(Renderer). 백그라운드 턴 종료는 한 줄 notice.
|
|
8
8
|
* - 실행 중 입력: 텍스트는 다음 턴 스티어링 큐로, ctrl-c는 현재 턴 중단.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* /spawn <agent> [task] 백그라운드 서브세션 생성(+실행)
|
|
10
|
+
* 실행 관찰:
|
|
12
11
|
* /sessions · /tree 세션 표 / 부모-자식 트리
|
|
13
12
|
* /s <n> · /switch <n> 활성 세션 전환 (tail 재생 + 라이브 구독)
|
|
14
|
-
* /steer <n> <msg> 해당 세션 다음 턴 큐잉
|
|
15
13
|
* /kill <n> · /rm <n> 턴 중단 / 세션 제거
|
|
16
|
-
*
|
|
14
|
+
* 서브에이전트 배정과 지시는 프로젝트 컨트롤러만 수행한다.
|
|
17
15
|
*/
|
|
18
16
|
const readline = require("node:readline");
|
|
19
17
|
const { renderBanner, readVersion } = require("../agentlas-banner.cjs");
|
|
20
18
|
const { Orchestrator, maxParallel } = require("../sessions/orchestrator.cjs");
|
|
21
19
|
const { Renderer } = require("./renderer.cjs");
|
|
22
|
-
const {
|
|
20
|
+
const { listAgents } = require("../agents/registry.cjs");
|
|
23
21
|
const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
|
|
24
22
|
const { EFFORTS } = require("../agentlas-workload-routing.cjs");
|
|
25
23
|
const permissions = require("../agentlas-permissions.cjs");
|
|
26
24
|
const i18n = require("../agentlas-i18n.cjs");
|
|
27
25
|
const { tokenizeCommandLine } = require("../agentlas-input.cjs");
|
|
28
|
-
|
|
29
|
-
const DEFAULT_AGENT_SLUG = "agentlas-orchestrator";
|
|
26
|
+
const { resolveProjectController, withProjectControllerContext } = require("../project/controller.cjs");
|
|
30
27
|
|
|
31
28
|
/*
|
|
32
29
|
* Shift-Tab 권한 순환 — 배너(banner.location)와 permCycleHint/permFullArm/
|
|
@@ -83,12 +80,9 @@ function createPermissionShortcut(opts = {}) {
|
|
|
83
80
|
};
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
function
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const builtin = findAgent(db, DEFAULT_AGENT_SLUG);
|
|
90
|
-
if (builtin) return builtin;
|
|
91
|
-
return null;
|
|
83
|
+
function pickProjectController(db, cwd = process.cwd()) {
|
|
84
|
+
const resolved = resolveProjectController(db, cwd);
|
|
85
|
+
return withProjectControllerContext(resolved.controller, resolved.project);
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
async function startRepl(ctx, opts = {}) {
|
|
@@ -124,7 +118,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
124
118
|
} catch (e) {
|
|
125
119
|
// 온보딩 실패는 REPL 진입을 막지 않지만, 조용히 삼키면 마법사 프롬프트와
|
|
126
120
|
// REPL이 stdin을 경합하는 사고(실사용 테스트에서 실증)가 위장된다 — 표시한다.
|
|
127
|
-
ctx.err(ui.c.dim(
|
|
121
|
+
ctx.err(ui.c.dim(en ? "One is recovering setup." : "One이 설정을 복구하고 있습니다."));
|
|
128
122
|
} finally {
|
|
129
123
|
wizardRl.close();
|
|
130
124
|
}
|
|
@@ -137,6 +131,21 @@ async function startRepl(ctx, opts = {}) {
|
|
|
137
131
|
let modelOverride = opts.model || null;
|
|
138
132
|
let effortOverride = opts.effort || null;
|
|
139
133
|
|
|
134
|
+
const recoverPresentation = (operation, error) => {
|
|
135
|
+
const active = orch.active();
|
|
136
|
+
const evidence = String((error && error.message) || error || "").slice(0, 8000);
|
|
137
|
+
if (active && !active.isBusy()) {
|
|
138
|
+
orch.sendTo(active.key, [
|
|
139
|
+
`The user operation “${operation}” did not complete.`,
|
|
140
|
+
"Private evidence follows. Never quote codes, paths, provider text, or stack details to the user.",
|
|
141
|
+
evidence,
|
|
142
|
+
"Judge the situation, perform safe reversible recovery within current authority, verify the original outcome, then give only a concise useful result. Ask only when identity or an irreversible choice is required.",
|
|
143
|
+
].join("\n")).catch(() => {});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
ui.line(ui.c.dim(en ? "One is checking and recovering this operation." : "One이 상태를 확인하고 복구하고 있습니다."));
|
|
147
|
+
};
|
|
148
|
+
|
|
140
149
|
const resolveRt = (agentId = null) => resolveRuntimeForAgent({
|
|
141
150
|
db,
|
|
142
151
|
prefs: ctx.prefs,
|
|
@@ -148,12 +157,12 @@ async function startRepl(ctx, opts = {}) {
|
|
|
148
157
|
});
|
|
149
158
|
|
|
150
159
|
let resumeChatId = opts.chatId || null;
|
|
151
|
-
const ensureMainSession = (
|
|
152
|
-
const agent =
|
|
160
|
+
const ensureMainSession = () => {
|
|
161
|
+
const agent = orch.active() ? orch.active().agent : pickProjectController(db);
|
|
153
162
|
if (!agent) {
|
|
154
163
|
throw new Error(en
|
|
155
|
-
?
|
|
156
|
-
:
|
|
164
|
+
? "This project has no available controller. Configure its ordered team in Desktop Work."
|
|
165
|
+
: "이 프로젝트에 실행 가능한 컨트롤러가 없습니다. Desktop Work에서 순서가 있는 팀을 설정하세요.");
|
|
157
166
|
}
|
|
158
167
|
const active = orch.active();
|
|
159
168
|
if (active && active.agent.id === agent.id) return active;
|
|
@@ -181,13 +190,13 @@ async function startRepl(ctx, opts = {}) {
|
|
|
181
190
|
try { runtimeLabel = resolveRt().kind; } catch { /* no_runtime: 첫 턴에서 정직 정지 */ }
|
|
182
191
|
let subjectLabel;
|
|
183
192
|
try {
|
|
184
|
-
const subject =
|
|
193
|
+
const subject = pickProjectController(db);
|
|
185
194
|
if (subject) subjectLabel = subject.slug;
|
|
186
195
|
} catch { /* 표시용 — 못 정해도 배너는 그린다 */ }
|
|
187
196
|
renderBanner({ ui, version: readVersion(), runtimeLabel, subjectLabel, permission, cwd: process.cwd() });
|
|
188
197
|
} catch (e) {
|
|
189
198
|
ctx.out(`agentlas ${readVersion()}`);
|
|
190
|
-
|
|
199
|
+
void e;
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
// 배너 카드가 런타임·권한·작업 폴더와 명령 메뉴를 이미 보여준다 — 남은 것만.
|
|
@@ -195,15 +204,6 @@ async function startRepl(ctx, opts = {}) {
|
|
|
195
204
|
? `v2 engine · parallel ≤${maxParallel()}`
|
|
196
205
|
: `v2 엔진 · 동시 ≤${maxParallel()}`));
|
|
197
206
|
|
|
198
|
-
if (opts.agent) {
|
|
199
|
-
try {
|
|
200
|
-
const session = ensureMainSession(opts.agent);
|
|
201
|
-
ctx.out(ui.c.dim(`agent: ${session.agent.slug} · ${session.runtime.kind}`));
|
|
202
|
-
} catch (e) {
|
|
203
|
-
ctx.err(String((e && e.message) || e));
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
207
|
return new Promise((resolve) => {
|
|
208
208
|
// 히스토리는 v1 input 모듈 재사용, 완성기는 v2 팔레트(ui/palette)가 정본이다.
|
|
209
209
|
const input = require("../agentlas-input.cjs");
|
|
@@ -325,7 +325,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
325
325
|
const runForeground = (session, text) => {
|
|
326
326
|
const p = orch.sendTo(session.key, text);
|
|
327
327
|
if (p && typeof p.then === "function") {
|
|
328
|
-
p.then(() => prompt(), (e) => {
|
|
328
|
+
p.then(() => prompt(), (e) => { recoverPresentation("project task", e); prompt(); });
|
|
329
329
|
}
|
|
330
330
|
};
|
|
331
331
|
|
|
@@ -350,14 +350,14 @@ async function startRepl(ctx, opts = {}) {
|
|
|
350
350
|
});
|
|
351
351
|
if (quit === "quit") { rl.close(); return; }
|
|
352
352
|
} catch (e) {
|
|
353
|
-
|
|
353
|
+
recoverPresentation("command", e);
|
|
354
354
|
}
|
|
355
355
|
prompt();
|
|
356
356
|
return;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
if (input.startsWith("!")) {
|
|
360
|
-
runShell(ctx, input.slice(1).trim(), permission).then(prompt, (e) => {
|
|
360
|
+
runShell(ctx, input.slice(1).trim(), permission).then(prompt, (e) => { recoverPresentation("shell action", e); prompt(); });
|
|
361
361
|
return;
|
|
362
362
|
}
|
|
363
363
|
|
|
@@ -366,7 +366,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
366
366
|
session = orch.active() || ensureMainSession(null);
|
|
367
367
|
if (!renderer.session) renderer.attach(session, { replay: false });
|
|
368
368
|
} catch (e) {
|
|
369
|
-
|
|
369
|
+
recoverPresentation("project startup", e);
|
|
370
370
|
prompt();
|
|
371
371
|
return;
|
|
372
372
|
}
|
|
@@ -379,7 +379,7 @@ async function startRepl(ctx, opts = {}) {
|
|
|
379
379
|
try {
|
|
380
380
|
runForeground(session, input);
|
|
381
381
|
} catch (e) {
|
|
382
|
-
|
|
382
|
+
recoverPresentation("project task", e);
|
|
383
383
|
prompt();
|
|
384
384
|
}
|
|
385
385
|
});
|
|
@@ -473,7 +473,7 @@ async function runShell(ctx, cmd, permission) {
|
|
|
473
473
|
ui.streamStart(true);
|
|
474
474
|
child.stdout.on("data", onData);
|
|
475
475
|
child.stderr.on("data", onData);
|
|
476
|
-
child.on("error", (
|
|
476
|
+
child.on("error", () => { ui.streamEnd(); ui.error(); resolve(); });
|
|
477
477
|
child.on("close", (code) => {
|
|
478
478
|
ui.streamEnd();
|
|
479
479
|
if (code !== 0 && !capped) ui.line(ui.c.dim(`exit ${code}`));
|
|
@@ -499,7 +499,7 @@ function printSessions(ctx, orch) {
|
|
|
499
499
|
const ui = ctx.uiInstance;
|
|
500
500
|
const rows = orch.list();
|
|
501
501
|
if (!rows.length) {
|
|
502
|
-
ctx.out(ui.c.dim(ctx.lang === "en" ? "no
|
|
502
|
+
ctx.out(ui.c.dim(ctx.lang === "en" ? "no project run yet — type a task" : "아직 프로젝트 실행이 없습니다 — 작업을 입력하세요"));
|
|
503
503
|
return;
|
|
504
504
|
}
|
|
505
505
|
for (const r of rows) {
|
|
@@ -532,7 +532,7 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
532
532
|
* 인자는 따옴표를 인식해 쪼갠다. 공백 분해는 따옴표를 인자 안에 그대로 남겨,
|
|
533
533
|
* 팔레트가 안내하는 그대로 `/search "무엇이 필요한지"` 를 치면 따옴표째 검색어가 됐다.
|
|
534
534
|
* 최상위 CLI 와 같은 토크나이저를 쓴다. restStr 은 원문 꼬리를 그대로 넘기는 자리
|
|
535
|
-
*
|
|
535
|
+
* 일부 패스스루 명령은 원문 꼬리를 그대로 전달한다.
|
|
536
536
|
*/
|
|
537
537
|
const rawCmd = cmdline.split(/\s+/)[0] || "";
|
|
538
538
|
const rest = tokenizeCommandLine(cmdline).slice(1);
|
|
@@ -546,7 +546,7 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
546
546
|
case "help": {
|
|
547
547
|
require("../commands/help.cjs").run(ctx, rest);
|
|
548
548
|
ctx.out("");
|
|
549
|
-
ctx.out(ui.c.bold(en ? "
|
|
549
|
+
ctx.out(ui.c.bold(en ? "Project Work runs" : "프로젝트 Work 실행"));
|
|
550
550
|
// 팔레트는 Tab 완성과 같은 정본(ui/palette)에서 렌더한다 — 목록 드리프트 금지.
|
|
551
551
|
ctx.out(require("./palette.cjs").renderPalette(ctx.lang));
|
|
552
552
|
ctx.out("");
|
|
@@ -558,7 +558,6 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
560
|
case "agents": case "list": require("../commands/list.cjs").run(ctx, rest); return;
|
|
561
|
-
case "chats": require("../commands/chats.cjs").run(ctx, rest); return;
|
|
562
561
|
case "doctor": require("../commands/doctor.cjs").run(ctx, rest); return;
|
|
563
562
|
case "mcp": require("../commands/mcp.cjs").run(ctx, rest); return;
|
|
564
563
|
|
|
@@ -571,36 +570,6 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
571
570
|
return;
|
|
572
571
|
}
|
|
573
572
|
|
|
574
|
-
case "spawn": {
|
|
575
|
-
const agentToken = rest[0];
|
|
576
|
-
if (!agentToken) throw new Error("Usage: /spawn <agent> [task]");
|
|
577
|
-
const agent = findAgent(ctx.db(), agentToken);
|
|
578
|
-
if (!agent) throw new Error((en ? "agent not found: " : "에이전트를 찾을 수 없음: ") + agentToken);
|
|
579
|
-
const task = restStr.slice(agentToken.length).trim();
|
|
580
|
-
const parent = orch.active();
|
|
581
|
-
const session = orch.spawn({
|
|
582
|
-
agent,
|
|
583
|
-
runtime: parent ? parent.runtime : api.resolveRt(),
|
|
584
|
-
permission: api.getPermission(),
|
|
585
|
-
cwd: process.cwd(),
|
|
586
|
-
parentKey: parent ? parent.key : null,
|
|
587
|
-
activate: false,
|
|
588
|
-
title: task ? `sub: ${task.slice(0, 60)}` : undefined,
|
|
589
|
-
});
|
|
590
|
-
if (task) orch.sendTo(session.key, task);
|
|
591
|
-
ctx.out(ui.c.dim(`${session.key} ${agent.slug} ${task ? (en ? "started" : "시작됨") : (en ? "ready (use /steer)" : "대기 (—/steer 로 지시)")}`));
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
case "steer": {
|
|
596
|
-
const key = sessionKeyArg(rest, `Usage: /${cmd} <n> <message>`);
|
|
597
|
-
const msg = restStr.slice(rest[0].length).trim();
|
|
598
|
-
if (!msg) throw new Error("Usage: /steer <n> <message>");
|
|
599
|
-
orch.sendTo(key, msg);
|
|
600
|
-
ctx.out(ui.c.dim(`→ ${key}`));
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
573
|
case "kill": {
|
|
605
574
|
const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
|
|
606
575
|
orch.kill(key);
|
|
@@ -614,27 +583,6 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
614
583
|
return;
|
|
615
584
|
}
|
|
616
585
|
|
|
617
|
-
case "broadcast": {
|
|
618
|
-
if (!restStr) throw new Error("Usage: /broadcast <message>");
|
|
619
|
-
/*
|
|
620
|
-
* broadcast 는 {sent, skipped} 를 준다. 전달된 목록을 먼저 찍고 못 보낸 세션은
|
|
621
|
-
* 사유와 함께 경고로 덧붙인다 — 예전엔 상한 throw 가 이 catch 로 떨어져 에러 한
|
|
622
|
-
* 줄만 보였고, 이미 지시를 받아 돌기 시작한 세션이 화면에 전혀 안 나왔다.
|
|
623
|
-
*/
|
|
624
|
-
const { sent, skipped } = orch.broadcast(restStr);
|
|
625
|
-
ctx.out(ui.c.dim(`→ ${sent.join(", ") || "(none)"}`));
|
|
626
|
-
for (const s of skipped) ui.warn(`${s.key} ${en ? "not sent" : "미전송"}: ${s.error}`);
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
case "use": {
|
|
631
|
-
if (!rest[0]) throw new Error("Usage: /use <agent>");
|
|
632
|
-
const session = ensureMainSession(rest[0]);
|
|
633
|
-
renderer.attach(session, { replay: false });
|
|
634
|
-
ctx.out(ui.c.dim(`agent: ${session.agent.slug}`));
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
586
|
case "runtime": {
|
|
639
587
|
if (!rest[0]) throw new Error("Usage: /runtime claude-code|codex|gemini");
|
|
640
588
|
api.setRuntime(rest[0]);
|
|
@@ -679,8 +627,8 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
679
627
|
* 제외: 자기 자신을 다시 여는 대화형 명령(chat/open/firm/setup)과 run
|
|
680
628
|
* (REPL의 평문 입력이 곧 run이다).
|
|
681
629
|
*/
|
|
682
|
-
// help/agents/list/
|
|
683
|
-
const REPL_EXCLUDED = new Set(["
|
|
630
|
+
// help/agents/list/mcp/doctor 등은 위 케이스에서 이미 처리된다.
|
|
631
|
+
const REPL_EXCLUDED = new Set(["firm", "setup", "run"]);
|
|
684
632
|
if (!REPL_EXCLUDED.has(cmd) && commands.COMMANDS[cmd]) {
|
|
685
633
|
const result = commands.COMMANDS[cmd]().run(ctx, rest);
|
|
686
634
|
if (result && typeof result.then === "function") {
|
|
@@ -32,7 +32,7 @@ const path = require("node:path");
|
|
|
32
32
|
const { userDataDir } = require("../core/paths.cjs");
|
|
33
33
|
const hubClient = require("../cloud/hub-client.cjs");
|
|
34
34
|
const detect = require("../runtimes/detect.cjs");
|
|
35
|
-
const {
|
|
35
|
+
const { roleMembers } = require("../runtimes/roles.cjs");
|
|
36
36
|
const capture = require("./capture.cjs");
|
|
37
37
|
|
|
38
38
|
// ── 런타임 해석 (v1 resolveRuntime의 워크포스 어댑터) ─────────────────────
|
|
@@ -115,8 +115,7 @@ function legacyWorkforceRuntime(db, override) {
|
|
|
115
115
|
throw error;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
//
|
|
119
|
-
// worker가 비어 있거나 inherit=1이면 roles.cjs가 orchestrator로만 승격한다.
|
|
118
|
+
// 명시 override 또는 Dashboard에 저장된 role priority만 실행 권위다.
|
|
120
119
|
function resolveWorkforceRuntime(db, override) {
|
|
121
120
|
if (override) {
|
|
122
121
|
const exact = legacyWorkforceRuntime(db, override);
|
|
@@ -129,9 +128,13 @@ function resolveWorkforceRuntime(db, override) {
|
|
|
129
128
|
},
|
|
130
129
|
};
|
|
131
130
|
}
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
131
|
+
const orchestrator = runtimeFromSelection(roleMembers(db, "orchestrator")[0] || null);
|
|
132
|
+
const worker = runtimeFromSelection(roleMembers(db, "worker")[0] || null);
|
|
133
|
+
if (!orchestrator || !worker) {
|
|
134
|
+
const error = new Error("Dashboard orchestrator and worker priorities are required for Workforce execution.");
|
|
135
|
+
error.code = "no_runtime_role_priority";
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
135
138
|
return {
|
|
136
139
|
...orchestrator,
|
|
137
140
|
role: "orchestrator",
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Agentlas
|
|
3
|
+
"version": "1.0.23",
|
|
4
|
+
"description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"smoke": "sh test/smoke.sh",
|
|
10
|
-
"test:release-contracts": "npm run smoke"
|
|
10
|
+
"test:release-contracts": "npm run smoke",
|
|
11
|
+
"sync:architecture": "node scripts/sync-architecture-from-desktop.cjs"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=20"
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* agentlas-doctor: 런타임 CLI 실패의 "시스템 원인"을 결정론적으로 진단·수리한다.
|
|
4
|
-
*
|
|
5
|
-
* 데스크탑 앱 electron/system-agents/runtime-doctor.ts 와 로직 패리티를 유지해야 한다
|
|
6
|
-
* (3제품 싱크: 데스크탑 TS ↔ 터미널 CJS ↔ system-optimizer 패키지 플레이북).
|
|
7
|
-
* 패리티는 Agentlas_F/scripts/sync-runtime-doctor.sh 가 공유 픽스처로 검증한다 —
|
|
8
|
-
* 이 파일의 분류/수리 규칙을 바꾸면 반드시 그 스크립트를 PASS 시켜라.
|
|
9
|
-
*
|
|
10
|
-
* 사례(2026-07-08): codex CLI 업데이트가 openai-curated 플러그인(notion/figma)을 자동
|
|
11
|
-
* 활성화 → 미인증 OAuth 원격 MCP가 매 실행 AuthRequired fatal → codex exit 1.
|
|
12
|
-
* 수리: 에러 stderr의 호스트와 플러그인 캐시 .mcp.json url 호스트를 대조해 "정확히 그
|
|
13
|
-
* 플러그인만" config.toml에서 enabled=false (백업 필수, 인증돼 잘 도는 플러그인 오폭 금지).
|
|
14
|
-
*/
|
|
15
|
-
const fs = require("node:fs");
|
|
16
|
-
const os = require("node:os");
|
|
17
|
-
const path = require("node:path");
|
|
18
|
-
|
|
19
|
-
function codexHome() {
|
|
20
|
-
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 에러 텍스트에서 OAuth 자원 메타데이터로 명시된 호스트만 추출한다.
|
|
25
|
-
* stderr의 모든 URL을 수집하면 도움말·문서 링크와 관련된 정상 플러그인까지
|
|
26
|
-
* 수리 대상이 될 수 있다. 자동 수리는 구조화된 증거가 있을 때만 실행한다.
|
|
27
|
-
*/
|
|
28
|
-
function extractHosts(error) {
|
|
29
|
-
const hosts = new Set();
|
|
30
|
-
const addUrlHost = (rawUrl) => {
|
|
31
|
-
try {
|
|
32
|
-
hosts.add(new URL(rawUrl.replace(/[\]}>),.;]+$/g, "")).hostname.toLowerCase());
|
|
33
|
-
} catch {
|
|
34
|
-
/* 잘못된 메타데이터 URL은 자동 수리 증거로 쓰지 않음 */
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const patterns = [
|
|
38
|
-
/resource_metadata(?:_url)?\s*[:=]\s*\\?["']?(https?:\/\/[^\s"'\\)>,]+)/gi,
|
|
39
|
-
/(https?:\/\/[^\s"'\\)>,]+\/\.well-known\/oauth-protected-resource(?:[/?#][^\s"'\\)>,]*)?)/gi,
|
|
40
|
-
];
|
|
41
|
-
for (const re of patterns) {
|
|
42
|
-
let m;
|
|
43
|
-
while ((m = re.exec(error || "")) !== null) addUrlHost(m[1]);
|
|
44
|
-
}
|
|
45
|
-
return [...hosts];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** 에러 텍스트에서 config.toml 스키마 위반이 지목한 mcp_servers 이름을 뽑는다. */
|
|
49
|
-
function extractBadMcpServer(error) {
|
|
50
|
-
// 예: Error loading config.toml: url is not supported for stdio in "mcp_servers.agentlas"
|
|
51
|
-
const m = /mcp_servers\.([a-z0-9_.-]+)/i.exec(error || "");
|
|
52
|
-
return m ? m[1] : null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** kind: mcp-oauth-unauthenticated | codex-config-invalid | timeout | cli-exit | unknown (데스크탑 TS와 동일 규칙) */
|
|
56
|
-
function classifyFailure(error) {
|
|
57
|
-
const text = error || "";
|
|
58
|
-
if (/authrequired|invalid_token|oauth-protected-resource|www_authenticate/i.test(text)) {
|
|
59
|
-
return { kind: "mcp-oauth-unauthenticated", hosts: extractHosts(text) };
|
|
60
|
-
}
|
|
61
|
-
// codex config.toml 파싱 실패(예: stdio 서버에 url 키) → CLI가 아예 기동 못 함.
|
|
62
|
-
if (/error loading config\.toml|url is not supported for stdio|invalid config/i.test(text)) {
|
|
63
|
-
return { kind: "codex-config-invalid", hosts: [], badServer: extractBadMcpServer(text) };
|
|
64
|
-
}
|
|
65
|
-
if (/no response for \d+s|auto-aborted/i.test(text)) return { kind: "timeout", hosts: [] };
|
|
66
|
-
if (/(?:CLI exit|exited with code)\s+[1-9]\d*/i.test(text)) return { kind: "cli-exit", hosts: extractHosts(text) };
|
|
67
|
-
return { kind: "unknown", hosts: [] };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** 실패 호스트와 일치하는 OAuth MCP를 실은 플러그인 찾기(호스트가 에러에 등장한 것만). */
|
|
71
|
-
function findOauthPluginsByHost(hosts) {
|
|
72
|
-
if (!hosts.length) return [];
|
|
73
|
-
const cacheRoot = path.join(codexHome(), "plugins", "cache");
|
|
74
|
-
const hits = [];
|
|
75
|
-
let marketplaces = [];
|
|
76
|
-
try {
|
|
77
|
-
marketplaces = fs.readdirSync(cacheRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
78
|
-
} catch {
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
for (const marketplace of marketplaces) {
|
|
82
|
-
let plugins = [];
|
|
83
|
-
try {
|
|
84
|
-
plugins = fs.readdirSync(path.join(cacheRoot, marketplace), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
85
|
-
} catch {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
for (const plugin of plugins) {
|
|
89
|
-
let versions = [];
|
|
90
|
-
try {
|
|
91
|
-
versions = fs.readdirSync(path.join(cacheRoot, marketplace, plugin), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
92
|
-
} catch {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
for (const ver of versions) {
|
|
96
|
-
const mcpJson = path.join(cacheRoot, marketplace, plugin, ver, ".mcp.json");
|
|
97
|
-
if (!fs.existsSync(mcpJson)) continue;
|
|
98
|
-
try {
|
|
99
|
-
const parsed = JSON.parse(fs.readFileSync(mcpJson, "utf8"));
|
|
100
|
-
for (const server of Object.values(parsed.mcpServers || {})) {
|
|
101
|
-
if (!server || !server.url) continue;
|
|
102
|
-
let host = "";
|
|
103
|
-
try {
|
|
104
|
-
host = new URL(server.url).hostname.toLowerCase();
|
|
105
|
-
} catch {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
// 호스트가 정확히 같을 때만 자동 수리한다. 부모/자식 도메인 관계만으로는
|
|
109
|
-
// 어느 플러그인이 실패했는지 증명할 수 없다.
|
|
110
|
-
if (hosts.includes(host)) {
|
|
111
|
-
// cache 디렉토리 "openai-curated-remote"는 config 키에선 "openai-curated".
|
|
112
|
-
hits.push({ pluginKey: `${plugin}@${marketplace.replace(/-remote$/, "")}`, host });
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} catch {
|
|
116
|
-
/* 손상된 .mcp.json은 건너뜀 */
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const seen = new Set();
|
|
122
|
-
return hits.filter((h) => (seen.has(h.pluginKey) ? false : (seen.add(h.pluginKey), true)));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** config.toml에서 해당 플러그인을 enabled=false로 내린다(백업 필수). 반환: 실제 변경 여부. */
|
|
126
|
-
function disableCodexPlugin(pluginKey) {
|
|
127
|
-
const configPath = path.join(codexHome(), "config.toml");
|
|
128
|
-
if (!fs.existsSync(configPath)) return false;
|
|
129
|
-
const original = fs.readFileSync(configPath, "utf8");
|
|
130
|
-
const header = `[plugins."${pluginKey}"]`;
|
|
131
|
-
let next;
|
|
132
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
133
|
-
const headerMatch = new RegExp(`^[ \\t]*${escapeRegExp(header)}[ \\t]*(?:#.*)?\\r?$`, "m").exec(original);
|
|
134
|
-
if (headerMatch) {
|
|
135
|
-
const headerLineEnd = headerMatch.index + headerMatch[0].length;
|
|
136
|
-
const newlineIndex = original.indexOf("\n", headerLineEnd);
|
|
137
|
-
const bodyStart = newlineIndex === -1 ? original.length : newlineIndex + 1;
|
|
138
|
-
const remaining = original.slice(bodyStart);
|
|
139
|
-
const nextSection = /^[ \t]*\[[^\r\n]+\][ \t]*(?:#.*)?\r?$/m.exec(remaining);
|
|
140
|
-
const sectionEnd = nextSection ? bodyStart + nextSection.index : original.length;
|
|
141
|
-
const section = original.slice(headerMatch.index, sectionEnd);
|
|
142
|
-
const replacedSection = section.replace(
|
|
143
|
-
/^([ \t]*enabled[ \t]*=[ \t]*)true([ \t]*(?:#.*)?)(?=\r?$)/m,
|
|
144
|
-
"$1false$2",
|
|
145
|
-
);
|
|
146
|
-
if (replacedSection === section) return false; // 이미 false거나 해당 섹션에 enabled=true가 없음
|
|
147
|
-
next = original.slice(0, headerMatch.index) + replacedSection + original.slice(sectionEnd);
|
|
148
|
-
} else {
|
|
149
|
-
next = `${original.trimEnd()}\n\n${header}\nenabled = false\n`;
|
|
150
|
-
}
|
|
151
|
-
const backup = `${configPath}.bak-doctor-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
152
|
-
fs.copyFileSync(configPath, backup);
|
|
153
|
-
fs.writeFileSync(configPath, next);
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* 진단 + (아는 계열이면) 즉시 수리. 반환: { kind, summary, repaired, actions[] }
|
|
159
|
-
* 데스크탑 runtime-doctor.ts 의 runRuntimeDoctor 와 동일 계약.
|
|
160
|
-
*/
|
|
161
|
-
function runRuntimeDoctor(errorMessage) {
|
|
162
|
-
const { kind, hosts, badServer } = classifyFailure(errorMessage);
|
|
163
|
-
const actions = [];
|
|
164
|
-
|
|
165
|
-
if (kind === "codex-config-invalid") {
|
|
166
|
-
// 진단만 한다(자동 수리 안 함): 원격 MCP 항목이라 안전한 무손실 수리가 없고,
|
|
167
|
-
// 사용자가 어느 항목을 어떻게 고칠지 알아야 한다. 명확한 조치를 안내한다.
|
|
168
|
-
const where = badServer ? `[mcp_servers.${badServer}]` : "일부 [mcp_servers.*] 항목";
|
|
169
|
-
return {
|
|
170
|
-
kind,
|
|
171
|
-
summary: `codex의 ~/.codex/config.toml ${where} 이(가) 잘못돼 codex가 기동하지 못합니다(예: stdio 서버에 url 키). 다른 런타임(--runtime claude-code)으로 우회하거나 그 항목을 고치세요.`,
|
|
172
|
-
repaired: false,
|
|
173
|
-
actions,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (kind === "mcp-oauth-unauthenticated") {
|
|
178
|
-
const hitList = findOauthPluginsByHost(hosts);
|
|
179
|
-
let repairedAny = false;
|
|
180
|
-
for (const hit of hitList) {
|
|
181
|
-
try {
|
|
182
|
-
if (disableCodexPlugin(hit.pluginKey)) {
|
|
183
|
-
repairedAny = true;
|
|
184
|
-
actions.push({
|
|
185
|
-
title: `codex plugin disabled: ${hit.pluginKey}`,
|
|
186
|
-
detail: `미인증 OAuth MCP(${hit.host})가 런타임을 죽여서 ~/.codex/config.toml에서 비활성화했습니다(백업 생성). 이 서비스를 쓰려면 인증 후 다시 켜세요.`,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
} catch (err) {
|
|
190
|
-
actions.push({ title: `repair failed: ${hit.pluginKey}`, detail: err && err.message ? err.message : String(err) });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
kind,
|
|
195
|
-
summary: hitList.length
|
|
196
|
-
? `런타임에 미인증 OAuth MCP 플러그인(${hitList.map((h) => h.pluginKey).join(", ")})이 붙어 있어 CLI가 죽었습니다.`
|
|
197
|
-
: `An unauthenticated OAuth MCP (${hosts.join(", ") || "unknown host"}) crashed the runtime, but the responsible plugin could not be identified.`,
|
|
198
|
-
repaired: repairedAny,
|
|
199
|
-
actions,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (kind === "timeout") {
|
|
204
|
-
return {
|
|
205
|
-
kind,
|
|
206
|
-
summary: "실행이 장시간 무응답이라 자동 중단됐습니다. 대화형 인증 대기·stdin 블록·원격 MCP 행이 흔한 원인입니다.",
|
|
207
|
-
repaired: false,
|
|
208
|
-
actions,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (kind === "cli-exit") {
|
|
213
|
-
return {
|
|
214
|
-
kind,
|
|
215
|
-
summary: "런타임 CLI가 비정상 종료했지만 아는 수리 계열이 아닙니다.",
|
|
216
|
-
repaired: false,
|
|
217
|
-
actions,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return { kind: "unknown", summary: "", repaired: false, actions };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
module.exports = { classifyFailure, extractHosts, findOauthPluginsByHost, disableCodexPlugin, runRuntimeDoctor };
|
package/engine/commands/chat.cjs
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/* chat — 에이전트 지정 REPL 진입 별칭: agentlas chat <agent> (= agentlas <agent>) */
|
|
3
|
-
function run(ctx, args) {
|
|
4
|
-
const ko = ctx.lang === "ko";
|
|
5
|
-
if (!args[0]) {
|
|
6
|
-
ctx.err(ko ? "사용법: agentlas chat <agent>" : "Usage: agentlas chat <agent>");
|
|
7
|
-
return 1;
|
|
8
|
-
}
|
|
9
|
-
const { startRepl } = require("../ui/repl.cjs");
|
|
10
|
-
return startRepl(ctx, { agent: args[0] });
|
|
11
|
-
}
|
|
12
|
-
module.exports = { run };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/* chats [n] — 최근 대화 목록 (공유 DB, 데스크탑과 동일 데이터). */
|
|
3
|
-
|
|
4
|
-
function run(ctx, args) {
|
|
5
|
-
const n = Math.max(1, Math.min(100, Number(args[0]) || 15));
|
|
6
|
-
const db = ctx.db();
|
|
7
|
-
if (!ctx.tableExists(db, "chats")) {
|
|
8
|
-
ctx.out(ctx.lang === "en" ? "No chats yet." : "대화가 아직 없습니다.");
|
|
9
|
-
return 0;
|
|
10
|
-
}
|
|
11
|
-
const rows = db.prepare(
|
|
12
|
-
`SELECT c.id, c.title, c.updated_at, a.slug AS agent_slug
|
|
13
|
-
FROM chats c LEFT JOIN installed_agents a ON a.id = c.agent_id
|
|
14
|
-
WHERE c.archived_at IS NULL AND (c.kind IS NULL OR c.kind = 'user')
|
|
15
|
-
ORDER BY c.updated_at DESC LIMIT ?`,
|
|
16
|
-
).all(n);
|
|
17
|
-
if (!rows.length) {
|
|
18
|
-
ctx.out(ctx.lang === "en" ? "No chats yet." : "대화가 아직 없습니다.");
|
|
19
|
-
return 0;
|
|
20
|
-
}
|
|
21
|
-
for (const r of rows) {
|
|
22
|
-
const when = String(r.updated_at || "").replace("T", " ").slice(0, 16);
|
|
23
|
-
// id 앞 8자를 먼저 찍는다 — `open <id 앞부분>`이 유일한 재개 경로인데
|
|
24
|
-
// 목록에 id가 없으면 SQLite를 직접 열지 않는 한 재개가 불가능했다.
|
|
25
|
-
// 8자는 open.cjs의 모호성 안내(id.slice(0,8))와 같은 규약.
|
|
26
|
-
const id = String(r.id || "").slice(0, 8);
|
|
27
|
-
ctx.out(` ${ctx.ui.bold(id)} ${ctx.ui.dim(when)} ${ctx.ui.accent((r.agent_slug || "?").padEnd(20))} ${r.title}`);
|
|
28
|
-
}
|
|
29
|
-
ctx.out(ctx.ui.dim(ctx.lang === "en" ? " resume: agentlas open <id>" : " 재개: agentlas open <id>"));
|
|
30
|
-
return 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
module.exports = { run };
|