agentlas 1.0.22 → 1.0.24

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.
@@ -13,6 +13,9 @@ const crypto = require("node:crypto");
13
13
  const { EventEmitter } = require("node:events");
14
14
  const nativeHost = require("../agentlas-native-host.cjs");
15
15
  const permissions = require("../agentlas-permissions.cjs");
16
+ const { RUNTIME_BIN, whichSync } = require("../runtimes/detect.cjs");
17
+ const { CLI_EXECUTABLE_KINDS } = require("../runtimes/resolve.cjs");
18
+ const { roleMembers } = require("../runtimes/roles.cjs");
16
19
  const { EventSink } = require("./sink.cjs");
17
20
  const store = require("./store.cjs");
18
21
 
@@ -37,6 +40,7 @@ class Session extends EventEmitter {
37
40
  this.status = "idle"; // idle | running | done | failed | killed
38
41
  this.lastLine = "";
39
42
  this.lastError = null;
43
+ this._privateRecoveryEvidence = [];
40
44
  this.startedAt = null;
41
45
  this.endedAt = null;
42
46
  this.usage = null;
@@ -52,7 +56,7 @@ class Session extends EventEmitter {
52
56
 
53
57
  this.chatId = opts.chatId || store.createChat(this.db, {
54
58
  agentId: this.agent.id,
55
- title: opts.title || (opts.parent ? `sub: ${this.agent.slug}` : "New chat"),
59
+ title: opts.title || (opts.parent ? `sub: ${this.agent.slug}` : "New project task"),
56
60
  kind: opts.parent ? "division" : "user",
57
61
  parentChatId: opts.parent ? opts.parent.chatId : null,
58
62
  workingFolder: this.cwd,
@@ -76,6 +80,11 @@ class Session extends EventEmitter {
76
80
  this._sink = new EventSink({
77
81
  lang: this.lang,
78
82
  onEvent: (ev) => this._record(ev),
83
+ onPrivateEvidence: (text) => {
84
+ if (!text) return;
85
+ this._privateRecoveryEvidence.push(text.slice(0, 4000));
86
+ if (this._privateRecoveryEvidence.length > 16) this._privateRecoveryEvidence.shift();
87
+ },
79
88
  });
80
89
  }
81
90
 
@@ -143,16 +152,41 @@ class Session extends EventEmitter {
143
152
  }
144
153
  }
145
154
 
155
+ _nextRecoveryRuntime() {
156
+ const role = this.runtime.role === "worker" ? "worker" : "orchestrator";
157
+ const members = roleMembers(this.db, role);
158
+ const current = members.findIndex((member) =>
159
+ member.kind === this.runtime.kind &&
160
+ (member.model || null) === (this.runtime.model || null),
161
+ );
162
+ if (current < 0) return null;
163
+ for (const member of members.slice(current + 1)) {
164
+ if (!CLI_EXECUTABLE_KINDS.has(member.kind)) continue;
165
+ const bin = whichSync(RUNTIME_BIN[member.kind]);
166
+ if (!bin) continue;
167
+ return {
168
+ kind: member.kind,
169
+ bin,
170
+ model: member.model || undefined,
171
+ effort: member.effort || undefined,
172
+ role,
173
+ source: "model-role-pool-recovery",
174
+ };
175
+ }
176
+ return null;
177
+ }
178
+
146
179
  async _runTurn(prompt) {
147
180
  this.status = "running";
148
181
  this.startedAt = Date.now();
149
182
  this.lastError = null;
183
+ this._privateRecoveryEvidence.length = 0;
150
184
  this._record({ type: "turn-start", at: Date.now(), prompt });
151
185
  store.appendMessage(this.db, this.chatId, "user", prompt);
152
186
  // 데스크탑처럼 첫 프롬프트로 자동 제목 — "New chat"으로 남는 목록 방지(실사용 테스트 발견).
153
187
  try {
154
188
  const row = this.db.prepare("SELECT title FROM chats WHERE id=?").get(this.chatId);
155
- if (row && (row.title === "New chat" || !row.title)) {
189
+ if (row && (["New chat", "New project task"].includes(row.title) || !row.title)) {
156
190
  store.retitleChat(this.db, this.chatId, prompt.slice(0, 60));
157
191
  }
158
192
  } catch { /* 제목은 장식 — 실패해도 턴 진행 */ }
@@ -234,6 +268,33 @@ class Session extends EventEmitter {
234
268
  if (this._timeoutConfig) req.timeoutConfig = this._timeoutConfig;
235
269
  try {
236
270
  res = await nativeHost.runNativeTurn(req);
271
+ if (res && res.error && !res.text && !res.finalText) {
272
+ const nextRuntime = this._nextRecoveryRuntime();
273
+ if (nextRuntime) {
274
+ const privateEvidence = [...this._privateRecoveryEvidence, String(res.error)]
275
+ .filter(Boolean).join("\n").slice(0, 12000);
276
+ this.runtime = nextRuntime;
277
+ this.runtimeSession = {};
278
+ this.fingerprint = crypto.createHash("sha256")
279
+ .update(`${nextRuntime.kind}\n${this.agent.id}\n${this.agent.systemPrompt || ""}`)
280
+ .digest("hex");
281
+ res = await nativeHost.runNativeTurn({
282
+ ...req,
283
+ kind: nextRuntime.kind,
284
+ bin: nextRuntime.bin,
285
+ model: nextRuntime.model,
286
+ effort: nextRuntime.effort,
287
+ session: {},
288
+ prompt: [
289
+ prompt,
290
+ "",
291
+ "Private recovery evidence follows. Never repeat it to the user.",
292
+ privateEvidence,
293
+ "Inspect the complete situation, apply safe reversible recovery within the granted authority, verify it, and finish the original request. Ask one concise question only if user identity or an irreversible choice is required.",
294
+ ].join("\n"),
295
+ });
296
+ }
297
+ }
237
298
  } catch (e) {
238
299
  res = { text: "", session: req.session, error: (e && e.message) || String(e) };
239
300
  }
@@ -274,8 +335,8 @@ class Session extends EventEmitter {
274
335
  }
275
336
  if (res && res.error) {
276
337
  this.status = "failed";
277
- this.lastError = res.error;
278
- this._record({ type: "turn-end", at: Date.now(), ok: false, error: res.error });
338
+ this.lastError = null;
339
+ this._record({ type: "turn-end", at: Date.now(), ok: false, recoveryRequired: true });
279
340
  } else {
280
341
  this.status = "done";
281
342
  // 링버퍼 표시 이벤트에도 raw 대신 cleanText — 제어 블록이 패널/lastLine 에 새지 않게.
@@ -287,7 +348,7 @@ class Session extends EventEmitter {
287
348
  try {
288
349
  require("./apply-fences.cjs").applyReplyFences(this, parsedFences, { orch: this.orchestrator });
289
350
  } catch (e) {
290
- this._record({ type: "error", at: Date.now(), text: `fence apply failed: ${(e && e.message) || String(e)}` });
351
+ this._privateRecoveryEvidence.push(`fence apply failed: ${(e && e.message) || String(e)}`.slice(0, 4000));
291
352
  }
292
353
  }
293
354
  return res;
@@ -16,11 +16,12 @@ const i18n = require("../agentlas-i18n.cjs");
16
16
  const NOOP_PALETTE = new Proxy({}, { get: () => (s) => String(s) });
17
17
 
18
18
  class EventSink {
19
- constructor({ lang = "en", onEvent } = {}) {
19
+ constructor({ lang = "en", onEvent, onPrivateEvidence } = {}) {
20
20
  this.lang = lang;
21
21
  this.t = (key, ...args) => i18n.t(this.lang, key, ...args);
22
22
  this.c = NOOP_PALETTE;
23
23
  this._onEvent = onEvent || (() => {});
24
+ this._onPrivateEvidence = onPrivateEvidence || (() => {});
24
25
  this._streamOpen = false;
25
26
  }
26
27
 
@@ -30,10 +31,19 @@ class EventSink {
30
31
 
31
32
  status(text) { this._emit("status", { text: String(text || "") }); }
32
33
  warn(text) { this._emit("warn", { text: String(text || "") }); }
33
- error(text) { this._emit("error", { text: String(text || "") }); }
34
+ // Runtime/provider failures are recovery evidence, never presentation copy.
35
+ // Session owns the recovery loop and may give this evidence to the controller;
36
+ // Renderer must never receive it first.
37
+ error(text) { this._onPrivateEvidence(String(text || "")); }
34
38
  line(text) { this._emit("line", { text: String(text || "") }); }
35
39
  tool(name, summary) { this._emit("tool", { name: String(name || "tool"), summary: String(summary || "") }); }
36
- toolResult(text, ok) { this._emit("tool-result", { text: String(text || ""), ok: ok !== false }); }
40
+ toolResult(text, ok) {
41
+ if (ok === false) {
42
+ this._onPrivateEvidence(String(text || ""));
43
+ return;
44
+ }
45
+ this._emit("tool-result", { text: String(text || ""), ok: true });
46
+ }
37
47
  streamStart() {
38
48
  if (!this._streamOpen) { this._streamOpen = true; this._emit("stream-start", {}); }
39
49
  }
@@ -21,7 +21,7 @@ function createChat(db, { agentId, title, kind = "user", parentChatId = null, wo
21
21
  runWriteTransaction(db, () => {
22
22
  db.prepare(
23
23
  "INSERT INTO chats (id, agent_id, title, created_at, updated_at, kind, parent_chat_id, working_folder) VALUES (?,?,?,?,?,?,?,?)",
24
- ).run(id, agentId, title || "New chat", now, now, kind, parentChatId, workingFolder);
24
+ ).run(id, agentId, title || "New project task", now, now, kind, parentChatId, workingFolder);
25
25
  });
26
26
  return id;
27
27
  }
@@ -40,7 +40,7 @@ function appendMessage(db, chatId, role, text) {
40
40
 
41
41
  function retitleChat(db, chatId, title) {
42
42
  runWriteTransaction(db, () => {
43
- db.prepare("UPDATE chats SET title=?, updated_at=? WHERE id=?").run(String(title || "New chat").slice(0, 120), nowIso(), chatId);
43
+ db.prepare("UPDATE chats SET title=?, updated_at=? WHERE id=?").run(String(title || "New project task").slice(0, 120), nowIso(), chatId);
44
44
  });
45
45
  }
46
46
 
@@ -3,8 +3,8 @@
3
3
  * ui/palette — v2 슬래시 명령 정본 + readline 완성기.
4
4
  *
5
5
  * 왜 v1 input 모듈의 SLASH_COMMANDS를 쓰지 않는가: 그 목록은 v1 REPL 전용이라
6
- * v2에 없는 명령(/status /team /model /effort …)을 광고하고, v2의 오르카 명령
7
- * (/spawn /sessions /s /steer /kill /broadcast …)은 빠져 있었다. 팔레트가
6
+ * v2에 없는 명령(/status /team /model /effort …)을 광고한다. 프로젝트 Work에서는
7
+ * 컨트롤러만 서브에이전트를 배정하므로 사용자용 임의 spawn/steer/broadcast 없다.
8
8
  * 거짓말하면 사용자는 없는 기능을 부른다(실사용 Tab 테스트에서 실증).
9
9
  * 경로 완성만 v1 모듈의 completePath를 재사용한다.
10
10
  */
@@ -17,15 +17,10 @@ const SLASH_COMMANDS = [
17
17
  { command: "/tree", args: "", ko: "세션 트리", en: "Session tree" },
18
18
  { command: "/s", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
19
19
  { command: "/switch", args: "<n>", ko: "활성 세션 전환", en: "Switch active session" },
20
- { command: "/spawn", args: "<agent> [task]", ko: "서브에이전트 세션 생성", en: "Spawn a subagent session" },
21
- { command: "/steer", args: "<n> <msg>", ko: "해당 세션에 지시 큐잉", en: "Queue steering for a session" },
22
20
  { command: "/kill", args: "<n>", ko: "실행 중 턴 중단", en: "Interrupt a running turn" },
23
21
  { command: "/rm", args: "<n>", ko: "세션 제거", en: "Remove a session" },
24
- { command: "/broadcast", args: "<msg>", ko: "모든 세션에 지시", en: "Send to every session" },
25
- { command: "/use", args: "<agent>", ko: "메인 세션 에이전트 교체", en: "Switch the main agent" },
26
22
  { command: "/agents", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
27
23
  { command: "/list", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
28
- { command: "/chats", args: "[n]", ko: "최근 대화", en: "Recent conversations" },
29
24
  { command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
30
25
  { command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
31
26
  { command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
@@ -99,8 +94,7 @@ const RUNTIME_KINDS = ["claude-code", "codex", "gemini"];
99
94
  const EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
100
95
  const PERM_LEVELS = ["read", "write", "full"];
101
96
  // 세션 인자를 받는 명령 — 완성 후보를 살아있는 세션 키(s1, s2…)로 채운다.
102
- const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/steer", "/kill", "/rm"]);
103
- const AGENT_ARG_COMMANDS = new Set(["/use", "/spawn"]);
97
+ const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/kill", "/rm"]);
104
98
 
105
99
  function uniqStartsWith(list, prefix) {
106
100
  const p = String(prefix || "");
@@ -137,9 +131,6 @@ function makeCompleter(ctx = {}) {
137
131
  if (cmd === "/effort") return [uniqStartsWith(EFFORT_LEVELS, last), last];
138
132
  if (cmd === "/permission") return [uniqStartsWith(PERM_LEVELS, last), last];
139
133
  if (SESSION_ARG_COMMANDS.has(cmd) && tokens.length === 2) return [uniqStartsWith(getSessions(), last), last];
140
- if (AGENT_ARG_COMMANDS.has(cmd) && tokens.length === 2) {
141
- return [uniqStartsWith(getAgents().concat(getFirms()), last), last];
142
- }
143
134
  return [[], last];
144
135
  };
145
136
  }
@@ -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": ui.warn(`automation refused: ${ev.name} — ${ev.reason}`); return;
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": ui.warn(`delegate refused: ${ev.target} — ${ev.reason}`); return;
112
- case "fence-error": ui.error(`fence: ${ev.text}`); return;
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;
@@ -1,32 +1,29 @@
1
1
  "use strict";
2
2
  /*
3
- * ui/repl — v2 REPL (Claude Code 방식 + 오르카 멀티세션).
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
- * /broadcast <msg> 전 세션에 동일 지시
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 { findAgent, listAgents } = require("../agents/registry.cjs");
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 pickDefaultAgent(db) {
87
- const visible = listAgents(db);
88
- if (visible.length) return visible[0];
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((en ? "setup wizard failed: " : "설정 마법사 실패: ") + String((e && e.message) || e)));
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 = (agentToken) => {
152
- const agent = agentToken ? findAgent(db, agentToken) : (orch.active() ? orch.active().agent : pickDefaultAgent(db));
160
+ const ensureMainSession = () => {
161
+ const agent = orch.active() ? orch.active().agent : pickProjectController(db);
153
162
  if (!agent) {
154
163
  throw new Error(en
155
- ? (agentToken ? `agent not found: ${agentToken}` : "no installed agent (agentlas search/install first)")
156
- : (agentToken ? `에이전트를 찾을 없음: ${agentToken}` : "설치된 에이전트가 없습니다 (agentlas search/install 먼저)"));
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 = opts.agent ? findAgent(db, opts.agent) : pickDefaultAgent(db);
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
- ctx.err(ui.c.dim(`banner failed: ${(e && e.message) || e}`));
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) => { ui.error(String((e && e.message) || e)); prompt(); });
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
- ui.error(String((e && e.message) || e));
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) => { ui.error(String((e && e.message) || e)); prompt(); });
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
- ui.error(String((e && e.message) || e));
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
- ui.error(String((e && e.message) || e));
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", (e) => { ui.streamEnd(); ui.error(e.message); resolve(); });
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 sessions yet — type a task or /spawn <agent> <task>" : "세션 없음 — 작업을 입력하거나 /spawn <agent> <task>"));
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
- * (/spawn·/steer·/broadcast)라 계속 원문에서 자른다.
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 ? "In-REPL session control (Orca)" : "REPL 세션 조종 (오르카)"));
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/chats/mcp/doctor 등은 위 케이스에서 이미 처리된다.
683
- const REPL_EXCLUDED = new Set(["chat", "open", "firm", "setup", "run"]);
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 { resolvedModelRole } = require("../runtimes/roles.cjs");
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
- // 사다리: 명시 override > model_roles[role] > 레거시 prefs/active/PATH.
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 fallback = legacyWorkforceRuntime(db, null);
133
- const orchestrator = runtimeFromSelection(resolvedModelRole(db, "orchestrator")) || fallback;
134
- const worker = runtimeFromSelection(resolvedModelRole(db, "worker")) || orchestrator;
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.22",
4
- "description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
3
+ "version": "1.0.24",
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"