agentlas 1.0.11 → 1.0.14

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +4 -2
  3. package/bin/agentlas.cjs +17 -3
  4. package/engine/agentlas-config.cjs +25 -18
  5. package/engine/agentlas-core-harness.cjs +14 -1
  6. package/engine/agentlas-i18n.cjs +2 -0
  7. package/engine/agentlas-input.cjs +62 -8
  8. package/engine/agentlas-memory-governance.cjs +10 -0
  9. package/engine/agentlas-onboard.cjs +22 -5
  10. package/engine/agentlas-sqlite-policy.cjs +18 -7
  11. package/engine/agentlas-workforce.cjs +989 -114
  12. package/engine/agentlas-workload-routing.cjs +61 -7
  13. package/engine/agentlas.cjs +8 -0
  14. package/engine/automation/daemon.cjs +76 -30
  15. package/engine/automation/schedule.cjs +16 -0
  16. package/engine/automation/store.cjs +92 -12
  17. package/engine/bootstrap-schema.sql +788 -29
  18. package/engine/commands/automation.cjs +8 -0
  19. package/engine/commands/chats.cjs +6 -1
  20. package/engine/commands/doctor.cjs +23 -1
  21. package/engine/commands/firm.cjs +66 -4
  22. package/engine/commands/help.cjs +31 -7
  23. package/engine/commands/hep-cloud.cjs +31 -0
  24. package/engine/commands/hep-hub.cjs +30 -0
  25. package/engine/commands/hep-local.cjs +32 -0
  26. package/engine/commands/hep-network.cjs +43 -0
  27. package/engine/commands/index.cjs +38 -7
  28. package/engine/commands/list.cjs +25 -1
  29. package/engine/commands/open.cjs +5 -1
  30. package/engine/commands/run.cjs +77 -13
  31. package/engine/commands/setup.cjs +12 -11
  32. package/engine/commands/storm.cjs +5 -9
  33. package/engine/commands/swarm.cjs +4 -4
  34. package/engine/commands/uninstall.cjs +36 -2
  35. package/engine/commands/version.cjs +29 -0
  36. package/engine/commands/workforce.cjs +10 -10
  37. package/engine/core/schema-ensure.cjs +75 -0
  38. package/engine/experience/variant.cjs +46 -2
  39. package/engine/firms/orchestrate.cjs +10 -3
  40. package/engine/hephaestus/runtime.cjs +7 -0
  41. package/engine/memory-cli/curate.cjs +3 -7
  42. package/engine/project/memory-context.cjs +3 -7
  43. package/engine/project/state.cjs +7 -6
  44. package/engine/runtimes/overrides.cjs +91 -22
  45. package/engine/runtimes/resolve.cjs +38 -8
  46. package/engine/runtimes/roles.cjs +162 -0
  47. package/engine/sessions/orchestrator.cjs +43 -4
  48. package/engine/sessions/prompt.cjs +4 -7
  49. package/engine/sessions/session.cjs +88 -20
  50. package/engine/storm/swarm.cjs +40 -12
  51. package/engine/ui/palette.cjs +52 -0
  52. package/engine/ui/renderer.cjs +37 -0
  53. package/engine/ui/repl.cjs +78 -10
  54. package/engine/workforce/capture.cjs +199 -14
  55. package/engine/workforce/concurrency.cjs +41 -0
  56. package/engine/workforce/deps.cjs +145 -29
  57. package/package.json +1 -1
@@ -191,7 +191,7 @@ function create(deps) {
191
191
  return typeof text === "string" ? text : (text && text.text) || "";
192
192
  }
193
193
 
194
- function recordAllocation(task, stage, decision, resolution, parentTaskId = null) {
194
+ function recordAllocation(task, stage, decision, resolution, parentTaskId = null, usage = null) {
195
195
  const receipt = workloadRouting.createDecisionReceipt({
196
196
  taskId: `${stage}-${task.id || "synthesis"}`,
197
197
  parentTaskId,
@@ -199,6 +199,7 @@ function create(deps) {
199
199
  stage,
200
200
  decision,
201
201
  resolution,
202
+ usage,
202
203
  });
203
204
  try {
204
205
  workloadRouting.appendDecisionReceipt(
@@ -221,9 +222,9 @@ function create(deps) {
221
222
  availableModels: ctx.availableModels,
222
223
  maxTier: ctx.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
223
224
  });
224
- recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
225
225
  // 할당 거부 후 CLI 기본 모델로 조용히 실행 금지 — fail-closed (계약 테스트 고정).
226
226
  if (!resolution.ok) {
227
+ recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
227
228
  throw new Error(`model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
228
229
  }
229
230
  if (resolution.fallbackReason) {
@@ -238,17 +239,32 @@ function create(deps) {
238
239
  source: resolution.source,
239
240
  fallbackReason: resolution.fallbackReason || null,
240
241
  };
241
- if (selectedRuntime.mode === "cli") {
242
- return await D.captureRuntime(selectedRuntime.kind, system, prompt, {
243
- cwd,
244
- env,
245
- permission,
246
- model: resolution.model,
247
- effort: resolution.effort,
248
- });
242
+ let observed;
243
+ try {
244
+ observed = selectedRuntime.mode === "cli"
245
+ ? await D.captureRuntime(selectedRuntime.kind, system, prompt, {
246
+ cwd,
247
+ env,
248
+ permission,
249
+ model: resolution.model,
250
+ effort: resolution.effort,
251
+ envelope: true,
252
+ })
253
+ : await D.runApi(
254
+ selectedRuntime.backend,
255
+ resolution.model || selectedRuntime.model,
256
+ system,
257
+ prompt,
258
+ { envelope: true },
259
+ );
260
+ } catch (error) {
261
+ recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
262
+ throw error;
249
263
  }
250
- const text = await D.runApi(selectedRuntime.backend, resolution.model || selectedRuntime.model, system, prompt);
251
- return typeof text === "string" ? text : (text && text.text) || "";
264
+ const text = typeof observed === "string" ? observed : (observed && observed.text) || "";
265
+ const usage = observed && typeof observed === "object" ? observed.usage : null;
266
+ recordAllocation(task, stage, task.allocation, resolution, parentTaskId, usage);
267
+ return text;
252
268
  }
253
269
 
254
270
  const label = runtime.mode === "cli" ? runtime.kind : runtime.backend;
@@ -411,6 +427,18 @@ function create(deps) {
411
427
  if (args[i] === "--parallel" || args[i] === "-n") concurrency = Number(args[++i]);
412
428
  else rest.push(args[i]);
413
429
  }
430
+ // storm.cjs stormRun 의 leading-dash 가드와 동일 계약. 여기서 안 막으면 미지원·
431
+ // 오타 플래그(--permission, --model …)가 rest 에 남아 rest.join(" ") 로 목표가
432
+ // 되어버린다: 유료 플래너가 플래그 텍스트를 목표로 실제 실행되고, 플래그가
433
+ // 의도한 동작(예: 권한 상승)은 조용히 일어나지 않는다. 목표는 프롬프트까지
434
+ // 오염된다. 그래서 소비되지 않은 대시 토큰은 실행 전에 fail-closed 로 거절한다.
435
+ const strayFlag = rest.find((token) => String(token).startsWith("-"));
436
+ if (strayFlag) {
437
+ const ui = executionContext.ui || newUi();
438
+ ui.error(`unknown option ${strayFlag} — swarm accepts: --parallel N | -n N, --runtime <kind>`);
439
+ process.exitCode = 1;
440
+ return { ok: false, error: "unknown-option" };
441
+ }
414
442
  const r = await swarmRun(db, rest.join(" "), { ...executionContext, concurrency, runtimeOverride });
415
443
  if (!r.ok) process.exitCode = 1;
416
444
  return r;
@@ -29,6 +29,8 @@ const SLASH_COMMANDS = [
29
29
  { command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
30
30
  { command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
31
31
  { command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
32
+ { command: "/model", args: "<id|default>", ko: "새 세션 모델 지정", en: "Set model for new sessions" },
33
+ { command: "/effort", args: "<level|none>", ko: "새 세션 추론 강도 지정", en: "Set effort for new sessions" },
32
34
  { command: "/permission", args: "<level>", ko: "새 세션 권한 지정", en: "Set permission for new sessions" },
33
35
  { command: "/login", args: "", ko: "Agentlas Cloud 로그인", en: "Sign in to Agentlas Cloud" },
34
36
  { command: "/whoami", args: "", ko: "로그인 상태", en: "Signed-in account" },
@@ -40,12 +42,61 @@ const SLASH_COMMANDS = [
40
42
  { command: "/storm", args: "<goal>", ko: "Goal+UltraCode 하니스", en: "Goal+UltraCode harness" },
41
43
  { command: "/swarm", args: "<goal>", ko: "에이전트 스웜", en: "Agent swarm" },
42
44
  { command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
45
+ { command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
46
+ { command: "/taskforce", args: "<request>", ko: "임시 태스크포스 편성", en: "Assemble a task force" },
47
+ // 소스 스코프가 이름에 붙은 스태핑 3종. 스코프를 문장에 적는다 — 예전에는
48
+ // 별칭이 스코프를 버려 /hep-cloud 가 자산 보관함으로, /hep-hub 가 검색으로
49
+ // 갔다(2026-07-28 수리). 팔레트 문구가 곧 사용자에게 하는 약속이다.
50
+ { command: "/hep-network", args: "\"<request>\"", ko: "로컬+오너 클라우드+공개 Hub 연합 편성", en: "Staff across Local + owner Cloud + public Hub" },
51
+ { command: "/hep-local", args: "\"<request>\"", ko: "등록된 로컬 에이전트만으로 편성", en: "Staff from registered Local agents only" },
52
+ { command: "/hep-cloud", args: "\"<request>\"", ko: "오너 Agent Cloud만으로 편성", en: "Staff from owner Agent Cloud only" },
53
+ { command: "/hep-hub", args: "\"<request>\"", ko: "공개 Hub 에이전트만으로 편성", en: "Staff from public Hub agents only" },
54
+ { command: "/build", args: "\"<request>\"", ko: "에이전트·팀 제작/수리/패키징", en: "Build, repair or package an agent or team" },
55
+ { command: "/call", args: "\"a,b\" \"<ctx>\"", ko: "지정 에이전트 호출", en: "Call named agents" },
56
+ { command: "/route", args: "\"<req>\"", ko: "최적 에이전트 라우팅", en: "Route to the best agent" },
57
+ { command: "/browser", args: "[sub]", ko: "브라우저 하드포인트", en: "Browser hardpoint" },
58
+ { command: "/connect", args: "<target>", ko: "에이전트·팀 연결", en: "Connect an agent or team" },
59
+ { command: "/research", args: "<sub>", ko: "리서치", en: "Research" },
60
+ { command: "/upload", args: "<path>", ko: "Agent Cloud에 저장·발행", en: "Save to Agent Cloud or publish" },
61
+ { command: "/cloud", args: "<sub>", ko: "클라우드 자산 관리", en: "Cloud assets" },
62
+ { command: "/import", args: "<path>", ko: "로컬 폴더 에이전트 가져오기", en: "Import a local folder agent" },
63
+ { command: "/cd", args: "[path]", ko: "작업 폴더 이동", en: "Change working folder" },
64
+ { command: "/native", args: "prepare <agent>", ko: "네이티브 CLI 컨텍스트 생성", en: "Prepare native CLI context" },
65
+ { command: "/plugin", args: "<sub>", ko: "Hub 플러그인(MCP)", en: "Hub plugins (MCP servers)" },
66
+ { command: "/plugins", args: "", ko: "설치된 플러그인", en: "Installed plugins" },
67
+ { command: "/experience", args: "<sub>", ko: "이식 가능한 Experience", en: "Portable Experience" },
68
+ { command: "/variant", args: "resolve", ko: "로컬 변형 선택", en: "Local variant selection" },
69
+ { command: "/memory", args: "<sub>", ko: "메모리", en: "Memory" },
70
+ { command: "/evolve", args: "", ko: "프롬프트 진화 제안", en: "Prompt-evolution proposals" },
71
+ // 데스크탑의 `ontology` 는 Core 의 지식·메모리 **런타임**(임베딩 포함)이고, 터미널의
72
+ // 이것은 **이 프로젝트의 지식 소스 등록부**다. 서로 다른 것이 같은 이름을 쓰고 있어
73
+ // (감사 D6) 라벨이라도 정확해야 한다 — 명령 이름은 사용자 습관과 스크립트가 걸려
74
+ // 있어 바꾸지 않는다. Core 의 지식 런타임은 터미널에 아직 미노출이다(결함 아님).
75
+ { command: "/ontology", args: "", ko: "프로젝트 지식 소스 등록", en: "Project knowledge sources" },
76
+ { command: "/career-graph", args: "", ko: "소스 라우팅 그래프", en: "Source routing graph" },
77
+ { command: "/journal", args: "<sub>", ko: "Stormbreaker 실행 일지", en: "Stormbreaker run journal" },
78
+ { command: "/project", args: "[status|init]", ko: ".agentlas 프로젝트 상태", en: "Private project state" },
79
+ { command: "/context", args: "<sub>", ko: "의존성 맵", en: "Dependency map" },
80
+ { command: "/creds", args: "<sub>", ko: "자격증명", en: "Credentials" },
81
+ { command: "/env", args: "", ko: "공유 환경 키", en: "Shared env keys" },
82
+ { command: "/multimodal", args: "", ko: "이미지·영상·음성 설정", en: "Image/video/audio providers" },
83
+ { command: "/telegram", args: "[sub]", ko: "텔레그램 연결", en: "Telegram bindings" },
84
+ { command: "/oberon", args: "[sub]", ko: "AI 필름", en: "AI film" },
85
+ { command: "/film", args: "<sub>", ko: "필름 렌더", en: "Film render" },
86
+ { command: "/hep", args: "<sub…>", ko: "Hephaestus 패스스루", en: "Hephaestus passthrough" },
87
+ { command: "/netadmin", args: "[sub]", ko: "로컬 네트워크 관리", en: "Local network admin" },
88
+ { command: "/update", args: "", ko: "npm 업데이트 확인", en: "npm update check" },
89
+ { command: "/version", args: "", ko: "버전", en: "Version" },
90
+ { command: "/logout", args: "", ko: "로그아웃", en: "Sign out" },
91
+ // 대화가 있으면 --yes 없이는 거절한다(챗/메시지 CASCADE 삭제) — 팔레트에도 노출.
92
+ { command: "/uninstall", args: "<slug> [--yes]", ko: "에이전트 제거", en: "Uninstall an agent" },
43
93
  { command: "/quit", args: "", ko: "종료", en: "Quit" },
44
94
  { command: "/exit", args: "", ko: "종료", en: "Quit" },
45
95
  ];
46
96
 
47
97
  const SLASH_NAMES = SLASH_COMMANDS.map((c) => c.command);
48
98
  const RUNTIME_KINDS = ["claude-code", "codex", "gemini"];
99
+ const EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
49
100
  const PERM_LEVELS = ["read", "write", "full"];
50
101
  // 세션 인자를 받는 명령 — 완성 후보를 살아있는 세션 키(s1, s2…)로 채운다.
51
102
  const SESSION_ARG_COMMANDS = new Set(["/s", "/switch", "/steer", "/kill", "/rm"]);
@@ -83,6 +134,7 @@ function makeCompleter(ctx = {}) {
83
134
 
84
135
  const cmd = tokens[0];
85
136
  if (cmd === "/runtime") return [uniqStartsWith(RUNTIME_KINDS, last), last];
137
+ if (cmd === "/effort") return [uniqStartsWith(EFFORT_LEVELS, last), last];
86
138
  if (cmd === "/permission") return [uniqStartsWith(PERM_LEVELS, last), last];
87
139
  if (SESSION_ARG_COMMANDS.has(cmd) && tokens.length === 2) return [uniqStartsWith(getSessions(), last), last];
88
140
  if (AGENT_ARG_COMMANDS.has(cmd) && tokens.length === 2) {
@@ -77,6 +77,43 @@ class Renderer {
77
77
  case "task-result": ui.applyTaskResult(ev.name, ev.result, ev.id); return;
78
78
  case "cost": ui.cost(ev.usage); return;
79
79
  case "queued": ui.line(ui.c.dim(` ↳ queued for next turn: ${ev.text}`)); return;
80
+
81
+ /* ── 펜스 영수증 (apply-fences.cjs) ──────────────────────────────────
82
+ * WHY: 펜스 블록은 cleanText 에서 통째로 제거되므로, 이 case 들이 없으면
83
+ * default 로 떨어져 화면에 아무 흔적도 남지 않는다. 그 결과 이미 enabled
84
+ * 상태로 등록된 반복 자동화(데몬/데스크탑 스케줄러가 실제로 실행한다)나
85
+ * 스폰된 위임 세션이 사용자 모르게 생기고, 에이전트의 확인 질문(ask)은
86
+ * 본문에서 삭제된 채 영영 묻지 않는다. 부작용에는 반드시 영수증이 따른다 —
87
+ * 새 펜스 이벤트를 apply-fences 에 추가하면 여기에도 case 를 추가할 것. */
88
+ case "ask": {
89
+ const p = ev.payload || {};
90
+ ui.ensureNl();
91
+ if (p.header) ui.line(ui.c.dim(` ${p.header}`));
92
+ ui.warn(p.question || "(question)");
93
+ (p.options || []).forEach((o, i) => {
94
+ const desc = o && o.description ? ` — ${o.description}` : "";
95
+ ui.line(ui.c.dim(` ${i + 1}. ${(o && o.label) || ""}${desc}`));
96
+ });
97
+ ui.line(ui.c.dim(` ↳ 답을 그대로 입력하세요${p.multiSelect ? " (복수 선택 가능)" : ""}`));
98
+ return;
99
+ }
100
+ case "automation-registered": {
101
+ const steps = ev.stepsIgnored ? ` · steps ${ev.stepsIgnored}개 무시(터미널은 그래프 합성 없음)` : "";
102
+ ui.ok(`automation registered: ${ev.name} · ${ev.schedule} · next ${ev.nextRunAt}${steps}`);
103
+ // 취소 경로를 함께 제시한다. 실제 서브커맨드는 off (commands/automation.cjs:125).
104
+ ui.line(ui.c.dim(` ↳ agentlas automation list · agentlas automation off ${String(ev.id || "").slice(0, 8)}`));
105
+ return;
106
+ }
107
+ case "automation-refused": ui.warn(`automation refused: ${ev.name} — ${ev.reason}`); return;
108
+ case "delegate-spawned":
109
+ ui.ok(`delegate spawned: ${ev.target} → ${ev.key}`);
110
+ return;
111
+ case "delegate-refused": ui.warn(`delegate refused: ${ev.target} — ${ev.reason}`); return;
112
+ case "fence-error": ui.error(`fence: ${ev.text}`); return;
113
+ case "memory-curated":
114
+ ui.line(ui.c.dim(` ↳ memory: ${ev.written}/${ev.candidates} written (permission: ${ev.permission})`));
115
+ return;
116
+
80
117
  default: return;
81
118
  }
82
119
  }
@@ -20,7 +20,8 @@ const { renderBanner, readVersion } = require("../agentlas-banner.cjs");
20
20
  const { Orchestrator, maxParallel } = require("../sessions/orchestrator.cjs");
21
21
  const { Renderer } = require("./renderer.cjs");
22
22
  const { findAgent, listAgents } = require("../agents/registry.cjs");
23
- const { resolveRuntime, NoRuntimeError } = require("../runtimes/resolve.cjs");
23
+ const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
24
+ const { EFFORTS } = require("../agentlas-workload-routing.cjs");
24
25
  const permissions = require("../agentlas-permissions.cjs");
25
26
  const i18n = require("../agentlas-i18n.cjs");
26
27
  const { tokenizeCommandLine } = require("../agentlas-input.cjs");
@@ -133,8 +134,18 @@ async function startRepl(ctx, opts = {}) {
133
134
  const renderer = new Renderer(ui);
134
135
  let permission = permissions.normalize(opts.permission || (ctx.prefs && ctx.prefs.permission) || "write");
135
136
  let runtimeOverride = opts.runtime || null;
136
-
137
- const resolveRt = () => resolveRuntime({ db, prefs: ctx.prefs, explicit: runtimeOverride });
137
+ let modelOverride = opts.model || null;
138
+ let effortOverride = opts.effort || null;
139
+
140
+ const resolveRt = (agentId = null) => resolveRuntimeForAgent({
141
+ db,
142
+ prefs: ctx.prefs,
143
+ explicit: runtimeOverride,
144
+ model: modelOverride,
145
+ effort: effortOverride,
146
+ role: "orchestrator",
147
+ agentId,
148
+ });
138
149
 
139
150
  let resumeChatId = opts.chatId || null;
140
151
  const ensureMainSession = (agentToken) => {
@@ -146,7 +157,14 @@ async function startRepl(ctx, opts = {}) {
146
157
  }
147
158
  const active = orch.active();
148
159
  if (active && active.agent.id === agent.id) return active;
149
- const session = orch.spawn({ agent, runtime: resolveRt(), permission, cwd: process.cwd(), activate: true, chatId: resumeChatId });
160
+ const session = orch.spawn({
161
+ agent,
162
+ runtime: resolveRt(agent.id),
163
+ permission,
164
+ cwd: process.cwd(),
165
+ activate: true,
166
+ chatId: resumeChatId,
167
+ });
150
168
  resumeChatId = null; // 재개는 첫 세션에만 적용
151
169
  renderer.attach(session, { replay: false });
152
170
  return session;
@@ -318,7 +336,18 @@ async function startRepl(ctx, opts = {}) {
318
336
 
319
337
  if (input.startsWith("/")) {
320
338
  try {
321
- const quit = handleSlash(ctx, input.slice(1), { orch, renderer, ensureMainSession, resolveRt, track: trackCommand, setPermission: (p) => { permission = p; }, getPermission: () => permission, setRuntime: (r) => { runtimeOverride = r; } });
339
+ const quit = handleSlash(ctx, input.slice(1), {
340
+ orch,
341
+ renderer,
342
+ ensureMainSession,
343
+ resolveRt,
344
+ track: trackCommand,
345
+ setPermission: (p) => { permission = p; },
346
+ getPermission: () => permission,
347
+ setRuntime: (r) => { runtimeOverride = r; },
348
+ setModel: (model) => { modelOverride = model; },
349
+ setEffort: (effort) => { effortOverride = effort; },
350
+ });
322
351
  if (quit === "quit") { rl.close(); return; }
323
352
  } catch (e) {
324
353
  ui.error(String((e && e.message) || e));
@@ -482,6 +511,18 @@ function printSessions(ctx, orch) {
482
511
  }
483
512
  }
484
513
 
514
+ /*
515
+ * 세션 키 파싱. 인자가 없으면 사용법을 낸다 — 예전에는 `s${undefined}` 가 그대로
516
+ * 조립돼 `/kill` 이 "no such session: sundefined" 를, `/steer` 는 rest[0].length 에서
517
+ * 날 TypeError 를 냈다. 팔레트가 `/steer <n> <msg>` 라고 안내하므로 인자 없이 Enter 를
518
+ * 눌러 사용법을 보려는 것은 정상적인 탐색이다.
519
+ */
520
+ function sessionKeyArg(rest, usage) {
521
+ const token = rest[0];
522
+ if (!token) throw new Error(usage);
523
+ return String(token).startsWith("s") ? token : `s${token}`;
524
+ }
525
+
485
526
  function handleSlash(ctx, cmdline, api) {
486
527
  const en = ctx.lang === "en";
487
528
  const ui = ctx.uiInstance;
@@ -524,7 +565,7 @@ function handleSlash(ctx, cmdline, api) {
524
565
  case "sessions": case "tree": printSessions(ctx, orch); return;
525
566
 
526
567
  case "s": case "switch": {
527
- const key = rest[0] && rest[0].startsWith("s") ? rest[0] : `s${rest[0]}`;
568
+ const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
528
569
  const session = orch.setActive(key);
529
570
  renderer.attach(session, { replay: true });
530
571
  return;
@@ -552,7 +593,7 @@ function handleSlash(ctx, cmdline, api) {
552
593
  }
553
594
 
554
595
  case "steer": {
555
- const key = rest[0] && rest[0].startsWith("s") ? rest[0] : `s${rest[0]}`;
596
+ const key = sessionKeyArg(rest, `Usage: /${cmd} <n> <message>`);
556
597
  const msg = restStr.slice(rest[0].length).trim();
557
598
  if (!msg) throw new Error("Usage: /steer <n> <message>");
558
599
  orch.sendTo(key, msg);
@@ -561,12 +602,12 @@ function handleSlash(ctx, cmdline, api) {
561
602
  }
562
603
 
563
604
  case "kill": {
564
- const key = rest[0] && rest[0].startsWith("s") ? rest[0] : `s${rest[0]}`;
605
+ const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
565
606
  orch.kill(key);
566
607
  return;
567
608
  }
568
609
  case "rm": {
569
- const key = rest[0] && rest[0].startsWith("s") ? rest[0] : `s${rest[0]}`;
610
+ const key = sessionKeyArg(rest, `Usage: /${cmd} <n>`);
570
611
  orch.remove(key);
571
612
  const active = orch.active();
572
613
  if (active) renderer.attach(active, { replay: false });
@@ -575,8 +616,14 @@ function handleSlash(ctx, cmdline, api) {
575
616
 
576
617
  case "broadcast": {
577
618
  if (!restStr) throw new Error("Usage: /broadcast <message>");
578
- const sent = orch.broadcast(restStr);
619
+ /*
620
+ * broadcast 는 {sent, skipped} 를 준다. 전달된 목록을 먼저 찍고 못 보낸 세션은
621
+ * 사유와 함께 경고로 덧붙인다 — 예전엔 상한 throw 가 이 catch 로 떨어져 에러 한
622
+ * 줄만 보였고, 이미 지시를 받아 돌기 시작한 세션이 화면에 전혀 안 나왔다.
623
+ */
624
+ const { sent, skipped } = orch.broadcast(restStr);
579
625
  ctx.out(ui.c.dim(`→ ${sent.join(", ") || "(none)"}`));
626
+ for (const s of skipped) ui.warn(`${s.key} ${en ? "not sent" : "미전송"}: ${s.error}`);
580
627
  return;
581
628
  }
582
629
 
@@ -594,6 +641,27 @@ function handleSlash(ctx, cmdline, api) {
594
641
  ctx.out(ui.c.dim(`runtime: ${rest[0]} (${en ? "applies to new sessions" : "새 세션부터 적용"})`));
595
642
  return;
596
643
  }
644
+ case "model": {
645
+ const model = String(rest[0] || "").trim();
646
+ if (!model) throw new Error("Usage: /model <provider-model-id|default>");
647
+ const next = ["default", "inherit"].includes(model.toLowerCase()) ? null : model;
648
+ api.setModel(next);
649
+ ctx.out(ui.c.dim(
650
+ `model: ${next || "default"} (${en ? "applies to new sessions" : "새 세션부터 적용"})`,
651
+ ));
652
+ return;
653
+ }
654
+ case "effort": {
655
+ const effort = String(rest[0] || "").trim().toLowerCase();
656
+ if (!EFFORTS.includes(effort)) {
657
+ throw new Error(`Usage: /effort ${EFFORTS.join("|")}`);
658
+ }
659
+ api.setEffort(effort === "none" ? null : effort);
660
+ ctx.out(ui.c.dim(
661
+ `effort: ${effort} (${en ? "applies to new sessions" : "새 세션부터 적용"})`,
662
+ ));
663
+ return;
664
+ }
597
665
  case "permission": {
598
666
  if (!["read", "write", "full"].includes(String(rest[0] || ""))) {
599
667
  throw new Error("Usage: /permission read|write|full");