agentlas 1.0.12 → 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 (49) hide show
  1. package/CHANGELOG.md +39 -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 +1 -1
  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 +136 -19
  12. package/engine/agentlas-workload-routing.cjs +32 -4
  13. package/engine/agentlas.cjs +8 -0
  14. package/engine/automation/daemon.cjs +67 -30
  15. package/engine/automation/schedule.cjs +16 -0
  16. package/engine/automation/store.cjs +70 -12
  17. package/engine/bootstrap-schema.sql +788 -53
  18. package/engine/commands/automation.cjs +8 -0
  19. package/engine/commands/chats.cjs +6 -1
  20. package/engine/commands/firm.cjs +7 -0
  21. package/engine/commands/help.cjs +23 -2
  22. package/engine/commands/hep-cloud.cjs +31 -0
  23. package/engine/commands/hep-hub.cjs +30 -0
  24. package/engine/commands/hep-local.cjs +32 -0
  25. package/engine/commands/hep-network.cjs +43 -0
  26. package/engine/commands/index.cjs +38 -7
  27. package/engine/commands/open.cjs +5 -1
  28. package/engine/commands/run.cjs +12 -2
  29. package/engine/commands/setup.cjs +12 -11
  30. package/engine/commands/storm.cjs +5 -9
  31. package/engine/commands/swarm.cjs +4 -4
  32. package/engine/commands/uninstall.cjs +36 -2
  33. package/engine/commands/version.cjs +29 -0
  34. package/engine/commands/workforce.cjs +10 -10
  35. package/engine/core/schema-ensure.cjs +75 -0
  36. package/engine/experience/variant.cjs +46 -2
  37. package/engine/hephaestus/runtime.cjs +7 -0
  38. package/engine/memory-cli/curate.cjs +3 -7
  39. package/engine/project/memory-context.cjs +3 -7
  40. package/engine/project/state.cjs +7 -6
  41. package/engine/sessions/orchestrator.cjs +41 -3
  42. package/engine/sessions/prompt.cjs +4 -7
  43. package/engine/sessions/session.cjs +32 -0
  44. package/engine/storm/swarm.cjs +12 -0
  45. package/engine/ui/palette.cjs +14 -2
  46. package/engine/ui/renderer.cjs +37 -0
  47. package/engine/ui/repl.cjs +7 -1
  48. package/engine/workforce/concurrency.cjs +41 -0
  49. package/package.json +1 -1
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ /*
3
+ * core/schema-ensure — 공유 SQLite 에 대한 스키마 보정을 **커넥션당 한 번만** 한다.
4
+ *
5
+ * 왜 (2026-07-28 실측):
6
+ * `agentlas.sqlite` 는 데스크탑 앱과 공유하는 파일이다(제품 계약, `core/paths.cjs`).
7
+ * 그런데 터미널이 **매 턴** 그 파일에 DDL 을 날리고 있었다 —
8
+ * · `ALTER TABLE memory_entries ADD COLUMN context_json` (사본 3벌)
9
+ * · `CREATE TABLE IF NOT EXISTS terminal_memory_*` 4개 + 인덱스 5개
10
+ * 전부 `IF NOT EXISTS`/조건부라 결과는 멱등이지만, **쓰기 락을 잡는 것은 매번**이다.
11
+ * 데스크탑이 마이그레이션 트랜잭션 중이면 15초 busy_timeout 을 소진하고, 호출부가
12
+ * 전부 빈 catch 라 그 실패가 조용히 사라진다.
13
+ *
14
+ * 스키마 소유자는 데스크탑이다(`core/db.cjs:7-9`). 터미널의 이 보정은 "앱보다 먼저
15
+ * 깔린 DB" 같은 경우를 위한 안전망이지 상시 작업이 아니다. 프로세스가 사는 동안
16
+ * 한 번이면 충분하다.
17
+ *
18
+ * 왜 커넥션당인가 (프로세스당이 아니라):
19
+ * 테스트와 일부 명령이 임시 DB 를 따로 연다. 프로세스 단위로 기억하면 두 번째
20
+ * DB 가 보정을 건너뛰어 조용히 깨진다. WeakMap 이면 커넥션이 사라질 때 같이 사라진다.
21
+ */
22
+ const applied = new WeakMap();
23
+
24
+ /**
25
+ * `key` 작업을 이 커넥션에서 아직 안 했으면 실행한다.
26
+ * 실패는 삼키되 **기억하지 않는다** — 다음 호출이 다시 시도할 수 있어야 한다.
27
+ * (일시적 락 경합으로 실패한 것을 "완료"로 기억하면 스키마가 영구히 안 맞는다.)
28
+ */
29
+ function ensureOnce(db, key, fn) {
30
+ if (!db) return false;
31
+ let done = applied.get(db);
32
+ if (!done) {
33
+ done = new Set();
34
+ applied.set(db, done);
35
+ }
36
+ if (done.has(key)) return true;
37
+ try {
38
+ fn(db);
39
+ done.add(key);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function tableExists(db, name) {
47
+ try {
48
+ return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name));
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function columnExists(db, table, column) {
55
+ try {
56
+ return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * `memory_entries.context_json` 보정. 이전에는 이 함수가 세 파일에 각각 복제돼 있었고
64
+ * (`sessions/prompt.cjs`, `project/memory-context.cjs`, `memory-cli/curate.cjs`)
65
+ * 그중 하나는 매 턴 호출됐다. 하나로 합치고 커넥션당 1회로 줄인다.
66
+ */
67
+ function ensureMemoryContextColumn(db) {
68
+ return ensureOnce(db, "memory_entries.context_json", (conn) => {
69
+ if (tableExists(conn, "memory_entries") && !columnExists(conn, "memory_entries", "context_json")) {
70
+ conn.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
71
+ }
72
+ });
73
+ }
74
+
75
+ module.exports = { ensureOnce, ensureMemoryContextColumn, tableExists, columnExists };
@@ -139,6 +139,37 @@ function resolveVariantCandidates(options) {
139
139
  };
140
140
  }
141
141
 
142
+ /*
143
+ * usage/설명은 사람 출력 전용이다 — JSON 결과(schemaVersion v1)와 decision 어휘는
144
+ * 손대지 않는다. README·`agentlas help`가 광고하는 `agentlas variant resolve`는
145
+ * 인자 없이 부르면 항상 decision=error 로 끝나는데, 그동안 사람에게 보인 건
146
+ * 내부 enum 한 줄(`error: EXACT_BASE_RELEASE_REQUIRED`) 뿐이라 필요한 플래그
147
+ * 이름이 소스 주석에만 존재했다. 그래서 (1) error 코드마다 사람이 읽고 바로
148
+ * 고칠 수 있는 사유 + usage 를 붙이고, (2) help 서브커맨드를 뚫는다.
149
+ */
150
+ const VARIANT_USAGE_LINES = Object.freeze([
151
+ "usage: agentlas variant resolve [candidates.json] [--base-release <agent-release-id>]",
152
+ " [--prefer <variant-id>] [--no-base-only] [--json]",
153
+ " --base-release <id> required: the exact base agent release to resolve against.",
154
+ " May instead come from candidates.json's baseAgentReleaseId.",
155
+ " candidates.json variant candidates: a JSON array, or {baseAgentReleaseId, candidates[]}.",
156
+ " --prefer <id> preferred variant; picking another one reports decision=fallback.",
157
+ " --no-base-only fail instead of falling back to the base release with no Experience Pack.",
158
+ " --json emit the agentlas.terminal-variant-resolution.v1 document.",
159
+ "Local advisory preview only: it never authorizes execution, rental, or reputation.",
160
+ ]);
161
+
162
+ const VARIANT_ERROR_HINTS = Object.freeze({
163
+ EXACT_BASE_RELEASE_REQUIRED:
164
+ "no base agent release was given — pass --base-release <agent-release-id>, or a candidates.json carrying baseAgentReleaseId.",
165
+ NO_ELIGIBLE_VARIANT_AND_NO_BASE_FALLBACK:
166
+ "no candidate survived the checks above and --no-base-only forbids the base-only fallback.",
167
+ });
168
+
169
+ function variantUsage() {
170
+ return VARIANT_USAGE_LINES.join("\n");
171
+ }
172
+
142
173
  function renderVariantResolution(result) {
143
174
  const lines = [
144
175
  `VARIANT RESOLUTION: ${result.decision}`,
@@ -148,17 +179,29 @@ function renderVariantResolution(result) {
148
179
  if (result.decision === "selected") lines.push(`selected: ${result.selectedVariantId}`);
149
180
  else if (result.decision === "fallback") lines.push(`fallback selected: ${result.selectedVariantId}`);
150
181
  else if (result.decision === "base-only") lines.push(`base-only: ${result.baseAgentReleaseId} (no Experience Pack attached)`);
151
- else lines.push(`error: ${result.code}`);
182
+ else {
183
+ lines.push(`error: ${result.code}`);
184
+ const hint = VARIANT_ERROR_HINTS[result.code];
185
+ if (hint) lines.push(hint);
186
+ }
152
187
  if (result.fallbackOrder.length) lines.push(`next fallbacks: ${result.fallbackOrder.join(", ")}`);
153
188
  for (const excluded of result.excluded) lines.push(`excluded only ${excluded.variantId}: ${excluded.reasons.join(", ")}`);
154
189
  lines.push("Required MCP shortages exclude only the affected variant; they never create an agent-wide shortage.");
190
+ // 실패했을 때만 usage를 붙인다 — 성공 출력은 v1과 바이트 동일하게 유지한다.
191
+ if (result.decision === "error") lines.push("", variantUsage());
155
192
  return lines.join("\n");
156
193
  }
157
194
 
158
195
  function cmdVariant(options) {
159
196
  const args = options.args || [];
160
197
  const sub = args[0] || "resolve";
161
- if (sub !== "resolve") throw new Error(`unknown variant subcommand: ${sub} (resolve)`);
198
+ // help 탈출구: 상위 라우터가 -h/--help "help" 정규화하지만, 직접 호출도
199
+ // 받도록 셋 다 인정한다. usage는 성공(exit 0)이지 알 수 없는 서브커맨드가 아니다.
200
+ if (sub === "help" || sub === "--help" || sub === "-h") {
201
+ (options.out || console.log)(variantUsage());
202
+ return null;
203
+ }
204
+ if (sub !== "resolve") throw new Error(`unknown variant subcommand: ${sub} (resolve|help)`);
162
205
  const flags = parseSimpleFlags(args.slice(1));
163
206
  let candidates = [];
164
207
  let baseAgentReleaseId = flags["base-release"] || null;
@@ -192,5 +235,6 @@ module.exports = {
192
235
  validateVariantCandidate,
193
236
  resolveVariantCandidates,
194
237
  renderVariantResolution,
238
+ variantUsage,
195
239
  cmdVariant,
196
240
  };
@@ -40,6 +40,13 @@ const USAGE = Object.freeze({
40
40
  call: 'usage: agentlas call "<agent-slugs>" "<context>"',
41
41
  connect: "usage: agentlas connect [status|telegram|help]",
42
42
  hep: "usage: agentlas hep <subcommand> [args]",
43
+ // 소스 스코프를 지키는 스태핑 표면 3종. 이름이 곧 계약이라 usage 도 스코프를
44
+ // 문장으로 적는다 — 이전에는 별칭이 스코프를 버려 hep-cloud 가 자산 보관함
45
+ // usage 를, hep-hub 가 검색 목록을 뱉었다(2026-07-28 수리).
46
+ "hep-network": 'usage: agentlas hep-network "<request>" # Local + owner Cloud + public Hub, federated by Core',
47
+ "hep-local": 'usage: agentlas hep-local "<request>" # registered Local agents only',
48
+ "hep-cloud": 'usage: agentlas hep-cloud "<request>" # owner Agent Cloud agents only',
49
+ "hep-hub": 'usage: agentlas hep-hub "<request>" # public Agentlas Hub agents only',
43
50
  hephaestus: "usage: agentlas hephaestus <subcommand> [args]",
44
51
  journal: "usage: agentlas journal <status|verify|repair|gate> --run-id <id> | --journal <path>",
45
52
  "legacy-network": 'usage: agentlas legacy-network "<request>"',
@@ -31,13 +31,9 @@ function loadArch() {
31
31
  return _arch;
32
32
  }
33
33
 
34
- function ensureMemoryContextColumn(db) {
35
- try {
36
- if (tableExists(db, "memory_entries") && !columnExists(db, "memory_entries", "context_json")) {
37
- db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
38
- }
39
- } catch { /* ignore */ }
40
- }
34
+ // 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다 — 이 보정이 매 턴 공유 DB 에
35
+ // 쓰기 락을 잡고 있었다(2026-07-28). 소유자는 데스크탑 스키마다.
36
+ const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
41
37
 
42
38
  const SECRET_RE = [/\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}/, /AKIA[0-9A-Z]{16}/, /ghp_[A-Za-z0-9]{20,}/, /xox[baprs]-[A-Za-z0-9-]{10,}/, /-----BEGIN [A-Z ]*PRIVATE KEY-----/, /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i];
43
39
 
@@ -32,13 +32,9 @@ function sha(value) {
32
32
  return crypto.createHash("sha256").update(value).digest("hex");
33
33
  }
34
34
 
35
- function ensureMemoryContextColumn(db) {
36
- try {
37
- if (tableExists(db, "memory_entries") && !columnExists(db, "memory_entries", "context_json")) {
38
- db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
39
- }
40
- } catch { /* ignore */ }
41
- }
35
+ // 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다 — 이 보정이 매 턴 공유 DB 에
36
+ // 쓰기 락을 잡고 있었다(2026-07-28). 소유자는 데스크탑 스키마다.
37
+ const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
42
38
 
43
39
  function prefsLangCli() {
44
40
  try {
@@ -341,15 +341,16 @@ function activeProjectPath(db, options = {}) {
341
341
  return result.activated ? root : null;
342
342
  }
343
343
 
344
- // v1 시그니처 보존: permission/reason은 받되 절대 초기화 권한으로 쓰지 않는다.
345
344
  function ensureTerminalProjectForExecutionCli(db, projectPath, permission = "write", reason = "terminal-first-contact") {
346
345
  const root = terminalProjectCandidateCli(projectPath);
347
346
  if (!root) return null;
348
- // Every ordinary execution mode is passive. Only `agentlas project init`
349
- // may create .agentlas state or edit .gitignore; write/full permission is
350
- // authority for the requested task, not consent to initialize the folder.
351
- void permission;
352
- void reason;
347
+ // A real write-capable execution is the product's first-contact boundary.
348
+ // Install the same private, merge-only Core infrastructure in whichever
349
+ // project the user opened; never key this behavior to the Agentlas_F repo.
350
+ if (permission === "write" || permission === "full") {
351
+ return initializeTerminalProjectCli(db, root, reason);
352
+ }
353
+ // Read-only inspection remains passive and never creates files.
353
354
  const active = activeProjectPath(db, { projectPath: root });
354
355
  if (!active) return null;
355
356
  return initializedAgentlasProjectPathCli(root);
@@ -117,6 +117,27 @@ class Orchestrator extends EventEmitter {
117
117
  if (!session) return;
118
118
  if (session.isBusy()) session.kill();
119
119
  this.sessions.delete(key);
120
+
121
+ // 부모를 지워도 자식 세션은 살아 있다(실행 중이며 병렬 슬롯을 쥔 채일 수 있다).
122
+ // 자식의 parent 포인터가 지워진 세션을 계속 가리키면 list()의 루트 판정
123
+ // (!s.parent)에서 탈락하고, 어떤 루트에서도 닿지 않아 /sessions·/tree 에서
124
+ // 통째로 사라진다 — 실행 중인데 보이지도 끌 수도 없는 유령 세션이 된다.
125
+ // 그래서 세션이 맵을 떠날 때 트리를 함께 정리한다: 남은 자식은 살아 있는
126
+ // 조부모로 승계하고(없으면 루트로 승격), 지워진 키는 부모 목록에서 뗀다.
127
+ const grandparent = session.parent && this.sessions.has(session.parent.key) ? session.parent : null;
128
+ for (const childKey of session.children) {
129
+ const child = this.sessions.get(childKey);
130
+ if (!child) continue;
131
+ child.parent = grandparent;
132
+ if (grandparent && !grandparent.children.includes(childKey)) grandparent.children.push(childKey);
133
+ }
134
+ session.children = [];
135
+ if (session.parent) {
136
+ const siblings = session.parent.children;
137
+ const at = siblings.indexOf(key);
138
+ if (at >= 0) siblings.splice(at, 1);
139
+ }
140
+
120
141
  if (this.activeKey === key) {
121
142
  const rest = [...this.sessions.keys()];
122
143
  this.activeKey = rest.length ? rest[rest.length - 1] : null;
@@ -124,14 +145,31 @@ class Orchestrator extends EventEmitter {
124
145
  this.emit("sessions-changed");
125
146
  }
126
147
 
148
+ /**
149
+ * 전 세션 브로드캐스트 — {sent, skipped}를 반환한다(throw 하지 않는다).
150
+ *
151
+ * WHY: 예전엔 루프 안에서 sendTo()의 throw가 그대로 올라갔다. 동시 상한(기본 4)보다
152
+ * 유휴 세션이 많으면 앞의 몇 개는 이미 프롬프트를 받아 실행을 시작한 뒤 상한 세션에서
153
+ * 터졌고, 그때까지 모은 sent 배열은 스택과 함께 버려졌다. 호출자(REPL)는 상한 에러
154
+ * 한 줄만 찍어 "전부 실패"로 보고했지만 실제로는 4개 세션이 그 지시를 받아 토큰을
155
+ * 쓰고 쓰기 작업까지 할 수 있는 상태였다 — 부분 성공을 전면 실패로 오보하는 것은
156
+ * 브로드캐스트에서 가장 위험한 거짓말이다.
157
+ * 그래서 실패는 세션 단위로 모으고, 실제 전달된 목록은 무슨 일이 있어도 반환한다.
158
+ * (sendTo/spawn의 "상한 초과는 정직한 거부" 계약 자체는 그대로 둔다.)
159
+ */
127
160
  broadcast(prompt) {
128
161
  const sent = [];
162
+ const skipped = [];
129
163
  for (const [key, session] of this.sessions) {
130
164
  if (session.status === "killed") continue;
131
- this.sendTo(key, prompt);
132
- sent.push(key);
165
+ try {
166
+ this.sendTo(key, prompt);
167
+ sent.push(key);
168
+ } catch (e) {
169
+ skipped.push({ key, error: String((e && e.message) || e) });
170
+ }
133
171
  }
134
- return sent;
172
+ return { sent, skipped };
135
173
  }
136
174
 
137
175
  /** 세션 표: [{key, active, agent, status, elapsed, lastLine, parentKey, depth}] */
@@ -88,13 +88,10 @@ function contextLine(json) {
88
88
  }
89
89
  }
90
90
 
91
- function ensureMemoryContextColumn(db) {
92
- try {
93
- if (tableExists(db, "memory_entries") && !columnExists(db, "memory_entries", "context_json")) {
94
- db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
95
- }
96
- } catch { /* 앱 마이그레이션 중이면 다음 턴에 */ }
97
- }
91
+ // 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다. 이 파일의 호출부는 **매 턴** 도는
92
+ // 자리라(cliMemoryContext), 여기가 공유 DB 에 쓰기 락을 가장 자주 잡던 지점이었다.
93
+ // 실패는 여전히 다음 호출에서 재시도된다 — 실패를 "완료" 기억하지 않는다.
94
+ const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
98
95
 
99
96
  /** Context Map 슬라이스 — Core 부재 시 정직하게 빈 문자열 (조작된 지도 금지). */
100
97
  function cliProjectContextSlice(projectPath, task) {
@@ -124,6 +124,25 @@ class Session extends EventEmitter {
124
124
  return result;
125
125
  }
126
126
 
127
+ /**
128
+ * 이미 동의된 시스템 MCP 서버 목록. `full` 권한이 아니면 빈 배열이다.
129
+ *
130
+ * 권한 확인을 여기서 한 번 더 하는 이유: native-host 도 `full` 게이트를 갖고
131
+ * 있지만, 권한이 낮은 턴에서 목록을 **읽는 것 자체를** 하지 않는 편이 낫다.
132
+ * 읽지 않은 값은 실수로 새 나갈 수 없다.
133
+ */
134
+ _consentedMcpServers() {
135
+ if (permissions.normalize(this.permission) !== "full") return [];
136
+ try {
137
+ const consent = require("../mcp/consent.cjs");
138
+ return consent.readConsentedSystemMcpServers(this.db, { env: process.env }) || [];
139
+ } catch {
140
+ // 동의 상태를 못 읽으면 MCP 없이 간다. 여기서 실패해 턴을 죽이면,
141
+ // MCP 는 부가 기능인데 대화 자체가 불가능해진다.
142
+ return [];
143
+ }
144
+ }
145
+
127
146
  async _runTurn(prompt) {
128
147
  this.status = "running";
129
148
  this.startedAt = Date.now();
@@ -196,6 +215,19 @@ class Session extends EventEmitter {
196
215
  session: { ...this.runtimeSession },
197
216
  model: this.runtime.model,
198
217
  effort: this.runtime.effort,
218
+ // 사용자가 이미 동의한 MCP 서버를 턴에 싣는다.
219
+ //
220
+ // 여기가 비어 있어서 챗 경로의 MCP 가 통째로 죽어 있었다(2026-07-28 확인:
221
+ // `mcpServers` 를 넘기는 호출자가 0곳). native-host 는 `full` 권한에서만
222
+ // 주입하도록 이미 게이팅돼 있었는데, 그 분기에 도달할 데이터를 아무도
223
+ // 채우지 않았다 — `agentlas mcp probe` 가 connected 를 찍어도 실제 턴에서는
224
+ // 그 서버를 쓸 수 없었다.
225
+ //
226
+ // 여기서 새로 묻지 않는다. `readConsentedSystemMcpServers` 는 이미 받아 둔
227
+ // 동의 영수증과 지문이 **지금도 일치하는** 서버만 돌려준다. 동의가 없으면
228
+ // 빈 배열이고, native-host 가 명시적 빈 격리로 간다. 턴 도중에 동의를 묻는
229
+ // 것은 사용자가 답할 수 없는 자리에서 묻는 것이라 하지 않는다.
230
+ mcpServers: this._consentedMcpServers(),
199
231
  onSpawn: (child) => { this._child = child; },
200
232
  };
201
233
  if (this._spawnImpl) req.spawn = this._spawnImpl;
@@ -427,6 +427,18 @@ function create(deps) {
427
427
  if (args[i] === "--parallel" || args[i] === "-n") concurrency = Number(args[++i]);
428
428
  else rest.push(args[i]);
429
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
+ }
430
442
  const r = await swarmRun(db, rest.join(" "), { ...executionContext, concurrency, runtimeOverride });
431
443
  if (!r.ok) process.exitCode = 1;
432
444
  return r;
@@ -44,6 +44,13 @@ const SLASH_COMMANDS = [
44
44
  { command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
45
45
  { command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
46
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" },
47
54
  { command: "/build", args: "\"<request>\"", ko: "에이전트·팀 제작/수리/패키징", en: "Build, repair or package an agent or team" },
48
55
  { command: "/call", args: "\"a,b\" \"<ctx>\"", ko: "지정 에이전트 호출", en: "Call named agents" },
49
56
  { command: "/route", args: "\"<req>\"", ko: "최적 에이전트 라우팅", en: "Route to the best agent" },
@@ -61,7 +68,11 @@ const SLASH_COMMANDS = [
61
68
  { command: "/variant", args: "resolve", ko: "로컬 변형 선택", en: "Local variant selection" },
62
69
  { command: "/memory", args: "<sub>", ko: "메모리", en: "Memory" },
63
70
  { command: "/evolve", args: "", ko: "프롬프트 진화 제안", en: "Prompt-evolution proposals" },
64
- { command: "/ontology", args: "", ko: "프로젝트 지식", en: "Project knowledge" },
71
+ // 데스크탑의 `ontology` Core 지식·메모리 **런타임**(임베딩 포함)이고, 터미널의
72
+ // 이것은 **이 프로젝트의 지식 소스 등록부**다. 서로 다른 것이 같은 이름을 쓰고 있어
73
+ // (감사 D6) 라벨이라도 정확해야 한다 — 명령 이름은 사용자 습관과 스크립트가 걸려
74
+ // 있어 바꾸지 않는다. Core 의 지식 런타임은 터미널에 아직 미노출이다(결함 아님).
75
+ { command: "/ontology", args: "", ko: "프로젝트 지식 소스 등록", en: "Project knowledge sources" },
65
76
  { command: "/career-graph", args: "", ko: "소스 라우팅 그래프", en: "Source routing graph" },
66
77
  { command: "/journal", args: "<sub>", ko: "Stormbreaker 실행 일지", en: "Stormbreaker run journal" },
67
78
  { command: "/project", args: "[status|init]", ko: ".agentlas 프로젝트 상태", en: "Private project state" },
@@ -77,7 +88,8 @@ const SLASH_COMMANDS = [
77
88
  { command: "/update", args: "", ko: "npm 업데이트 확인", en: "npm update check" },
78
89
  { command: "/version", args: "", ko: "버전", en: "Version" },
79
90
  { command: "/logout", args: "", ko: "로그아웃", en: "Sign out" },
80
- { command: "/uninstall", args: "<slug>", ko: "에이전트 제거", en: "Uninstall an agent" },
91
+ // 대화가 있으면 --yes 없이는 거절한다(챗/메시지 CASCADE 삭제) 팔레트에도 노출.
92
+ { command: "/uninstall", args: "<slug> [--yes]", ko: "에이전트 제거", en: "Uninstall an agent" },
81
93
  { command: "/quit", args: "", ko: "종료", en: "Quit" },
82
94
  { command: "/exit", args: "", ko: "종료", en: "Quit" },
83
95
  ];
@@ -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
  }
@@ -616,8 +616,14 @@ function handleSlash(ctx, cmdline, api) {
616
616
 
617
617
  case "broadcast": {
618
618
  if (!restStr) throw new Error("Usage: /broadcast <message>");
619
- const sent = orch.broadcast(restStr);
619
+ /*
620
+ * broadcast 는 {sent, skipped} 를 준다. 전달된 목록을 먼저 찍고 못 보낸 세션은
621
+ * 사유와 함께 경고로 덧붙인다 — 예전엔 상한 throw 가 이 catch 로 떨어져 에러 한
622
+ * 줄만 보였고, 이미 지시를 받아 돌기 시작한 세션이 화면에 전혀 안 나왔다.
623
+ */
624
+ const { sent, skipped } = orch.broadcast(restStr);
620
625
  ctx.out(ui.c.dim(`→ ${sent.join(", ") || "(none)"}`));
626
+ for (const s of skipped) ui.warn(`${s.key} ${en ? "not sent" : "미전송"}: ${s.error}`);
621
627
  return;
622
628
  }
623
629
 
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /*
3
+ * workforce/concurrency — 워크포스 워커 동시 실행 수(스웜 크기)의 사양 기반 추천값.
4
+ *
5
+ * 워커 1명 = captureRuntime을 통해 스폰되는 실제 CLI 자식 프로세스(engine/workforce/
6
+ * capture.cjs)다. 이전에는 사용자가 --parallel/-n을 안 주면 컴퓨터 사양과 무관하게
7
+ * 고정 3(상한 8)을 썼다 — 코어 2개짜리 저사양 기기에서 3개 CLI 자식이 한꺼번에 뜨면
8
+ * 과다 산정, 32코어 워크스테이션에서도 항상 3으로 저평가되는 양방향 문제였다.
9
+ *
10
+ * 데스크탑(electron/store/concurrency.ts)과 동일한 원리(코어 2개는 OS/자식 자신에게
11
+ * 남기고, 에이전트당 RAM ~2GB + 여유 4GB로 추정)를 쓰되, 상한(HARD_MAX)은 터미널
12
+ * 자체의 기존 안전선(8)을 그대로 유지한다 — 이건 provider가 바뀌어도 변하지 않는
13
+ * Agentlas 자체 정책 상수이지, 하드코딩 금지 대상인 "외부에서 바뀌는 값"이 아니다.
14
+ */
15
+ const os = require("node:os");
16
+
17
+ const HARD_MAX = 8;
18
+
19
+ function getSystemSpecs() {
20
+ let cores = 4;
21
+ let totalMemGB = 8;
22
+ try {
23
+ cores = Math.max(1, os.cpus().length);
24
+ } catch {
25
+ // fall back
26
+ }
27
+ try {
28
+ totalMemGB = os.totalmem() / 1024 ** 3;
29
+ } catch {
30
+ // fall back
31
+ }
32
+ return { cores, totalMemGB };
33
+ }
34
+
35
+ function recommendedConcurrency(specs = getSystemSpecs()) {
36
+ const coreBound = Math.max(1, specs.cores - 2);
37
+ const memBound = Math.max(1, Math.floor((specs.totalMemGB - 4) / 2));
38
+ return Math.max(1, Math.min(coreBound, memBound, HARD_MAX));
39
+ }
40
+
41
+ module.exports = { HARD_MAX, getSystemSpecs, recommendedConcurrency };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"