agentlas 0.7.0 → 0.9.2

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 +199 -0
  2. package/README.md +161 -18
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-core-harness.cjs +212 -0
  5. package/engine/agentlas-desktop-loadout.cjs +527 -0
  6. package/engine/agentlas-doctor.cjs +1 -1
  7. package/engine/agentlas-experience-exchange.cjs +835 -85
  8. package/engine/agentlas-experience-intake.cjs +444 -0
  9. package/engine/agentlas-experience-mcp.cjs +580 -18
  10. package/engine/agentlas-i18n.cjs +10 -10
  11. package/engine/agentlas-input.cjs +5 -4
  12. package/engine/agentlas-mcp-env.cjs +219 -0
  13. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  14. package/engine/agentlas-memory-governance.cjs +1029 -0
  15. package/engine/agentlas-native-host.cjs +129 -39
  16. package/engine/agentlas-parity.cjs +339 -154
  17. package/engine/agentlas-repl.cjs +306 -31
  18. package/engine/agentlas-workforce.cjs +2991 -0
  19. package/engine/agentlas-workload-routing.cjs +523 -0
  20. package/engine/agentlas.cjs +1619 -234
  21. package/engine/bootstrap-schema.sql +1 -1
  22. package/engine/experience-taxonomy-v1.json +49 -0
  23. package/package.json +8 -4
  24. package/scripts/gen-bootstrap-schema.sh +0 -23
  25. package/test/bootstrap-race.cjs +0 -47
  26. package/test/capture-runtime-guard.cjs +0 -122
  27. package/test/cloud-asset-restore.cjs +0 -423
  28. package/test/cloud-cas-client.cjs +0 -333
  29. package/test/cloud-owner-restore.cjs +0 -183
  30. package/test/cloud-runtime-paths.cjs +0 -40
  31. package/test/cloud-save-publish.cjs +0 -487
  32. package/test/credential-env-regression.cjs +0 -52
  33. package/test/engine-hardening-regression.cjs +0 -74
  34. package/test/experience-exchange-contract.cjs +0 -569
  35. package/test/experience-mcp-contract.cjs +0 -391
  36. package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
  37. package/test/login-loopback-security.cjs +0 -115
  38. package/test/mcp-config-isolation.cjs +0 -36
  39. package/test/permission-mapping.cjs +0 -180
  40. package/test/route-regression.cjs +0 -357
  41. package/test/run-api-regression.cjs +0 -322
  42. package/test/runtime-env-protection.cjs +0 -89
  43. package/test/semver-precedence.cjs +0 -39
  44. package/test/smoke.sh +0 -93
  45. package/test/sqlite-driver-probe.cjs +0 -22
  46. package/test/terminal-ui-regression.cjs +0 -477
  47. package/test/timeout-regression.cjs +0 -218
  48. package/test/tool-workspace-boundary.cjs +0 -165
  49. package/test/update-safety.cjs +0 -376
@@ -31,6 +31,11 @@ const crypto = require("node:crypto");
31
31
  const { compareSemVer, normalizeSemVer, parseSemVer } = require("./semver.cjs");
32
32
  const terminalAssets = require("./agentlas-experience-mcp.cjs");
33
33
  const terminalExperienceExchange = require("./agentlas-experience-exchange.cjs");
34
+ const desktopOntologyLoadout = require("./agentlas-desktop-loadout.cjs");
35
+ const workloadRouting = require("./agentlas-workload-routing.cjs");
36
+ const terminalExperienceIntake = require("./agentlas-experience-intake.cjs");
37
+ const terminalMemoryGovernance = require("./agentlas-memory-governance.cjs");
38
+ const { captureCoreJsonSync, resolveCoreRuntimeRoot } = require("./agentlas-core-harness.cjs");
34
39
 
35
40
  // ── 앱과 동일한 userData 경로 (electron app.getPath('userData')와 일치) ──
36
41
  function userDataDir() {
@@ -256,7 +261,7 @@ function saveMultimodalSettingsCli(db, patch) {
256
261
  db.prepare("INSERT INTO meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
257
262
  .run(MULTIMODAL_META_KEY, JSON.stringify(next));
258
263
  } catch (e) {
259
- fail("multimodal settings 저장 실패: " + e.message);
264
+ fail("Failed to save multimodal settings: " + e.message);
260
265
  }
261
266
  return next;
262
267
  }
@@ -388,8 +393,8 @@ function routeNormalize(value) {
388
393
  return String(value || "").toLowerCase().replace(/[_/]+/g, "-");
389
394
  }
390
395
  // 경로 디렉터리 성분은 라우팅 의도가 아니다 — 마지막 세그먼트(파일/폴더명)만 남긴다.
391
- // 사고(2026-07-12): "/Users/mason/Documents/…/Appbridge_Template.이 …" 프롬프트의 경로 토큰
392
- // ("users","mason","documents","users-mason-documents-")이 임포트 에이전트 system_prompt 속
396
+ // 사고(2026-07-12): "/Users/example/Projects/…/Appbridge_Template.이 …" 프롬프트의 경로 토큰
397
+ // ("users","example","projects","users-example-projects-")이 임포트 에이전트 system_prompt 속
393
398
  // 절대경로와 맞아떨어져 +2씩 쌓이고 라우팅 근거에까지 노출됐다. 프롬프트/헤이스택 양쪽에
394
399
  // 대칭 적용해 경로↔경로 우연 일치를 차단한다. 파일/폴더명은 실제 의도라서 보존한다.
395
400
  // 규칙: 공백/인용부호/괄호 뒤(또는 문자열 시작)에서 시작하고, "세그먼트+구분자"가 2회 이상
@@ -667,6 +672,40 @@ function agentFolder(agent) {
667
672
  if (exists(path.join(cloudRoot, CLOUD_RESTORE_MARKER_PATH))) return cloudRoot;
668
673
  return path.join(userDataDir(), "agents", agent.slug);
669
674
  }
675
+ function exactAgentBaseForExecution(db, agent, runtimeExperience = null) {
676
+ if (!agent || agent.builtin) return null;
677
+ const portableId = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
678
+ let binding = null;
679
+ try {
680
+ if (tableExists(db, "installed_agent_hub_bindings")) {
681
+ binding = db.prepare(
682
+ "SELECT agent_definition_id,agent_release_id FROM installed_agent_hub_bindings WHERE installed_agent_id=?",
683
+ ).get(agent.id) || null;
684
+ }
685
+ } catch { binding = null; }
686
+ const route = routesMap()[agent.id] || {};
687
+ const markerResult = terminalExperienceExchange.readExactLocalBaseMarker(agentFolder(agent), agent.slug);
688
+ const marker = markerResult.marker;
689
+ const rawHash = String(marker?.packageHash || route.packageHash || route.definitionHash || "").replace(/^sha256:/i, "").toLowerCase();
690
+ const packageHash = /^[a-f0-9]{64}$/.test(rawHash) ? `sha256:${rawHash}` : null;
691
+ const explicitDefinition = String(runtimeExperience?.agentDefinitionId || "");
692
+ const explicitRelease = String(runtimeExperience?.baseAgentReleaseId || "");
693
+ if (portableId.test(explicitDefinition) && portableId.test(explicitRelease)) {
694
+ return { agentDefinitionId: explicitDefinition, agentReleaseId: explicitRelease, packageHash, authority: "explicit-runtime-binding" };
695
+ }
696
+ if (binding && portableId.test(String(binding.agent_definition_id)) && portableId.test(String(binding.agent_release_id))) {
697
+ return { agentDefinitionId: binding.agent_definition_id, agentReleaseId: binding.agent_release_id, packageHash, authority: "installed-hub-binding" };
698
+ }
699
+ if (!packageHash) return null;
700
+ const definitionDigest = sha(`terminal-local-definition\0${agent.id}\0${agent.slug}`);
701
+ const releaseDigest = sha(`terminal-local-release\0${definitionDigest}\0${packageHash}`);
702
+ return {
703
+ agentDefinitionId: `local-agent-definition:${definitionDigest.slice(0, 32)}`,
704
+ agentReleaseId: `local-agent-release:${releaseDigest.slice(0, 32)}`,
705
+ packageHash,
706
+ authority: "exact-local-package-hash",
707
+ };
708
+ }
670
709
  function agentSystemPromptCli(agent) {
671
710
  return agent && agent.system_prompt ? agent.system_prompt : `You are ${agent?.name || "an Agentlas agent"}.`;
672
711
  }
@@ -774,7 +813,7 @@ function buildImportSystemPrompt(dir, name, kind) {
774
813
  }
775
814
  function importLocalFolderCli(db, absPath) {
776
815
  const dir = path.resolve(absPath);
777
- if (!isDir(dir)) fail(`폴더가 아닙니다: ${absPath}`);
816
+ if (!isDir(dir)) fail(`Not a directory: ${absPath}`);
778
817
  const labels = detectRuntimeLabels(dir);
779
818
  const runtime = labels[0];
780
819
  const kind = detectKind(dir);
@@ -879,15 +918,15 @@ function upsertLocalTeamFirmCli(db, dir, ceoAgentId, agentSlug, name, tagline) {
879
918
  return { id, slug: firmSlug };
880
919
  }
881
920
  function cmdImport(db, absPath) {
882
- if (!absPath) fail("사용법: agentlas import <폴더경로>");
921
+ if (!absPath) fail("Usage: agentlas import <folder-path>");
883
922
  const r = importLocalFolderCli(db, absPath);
884
- out(`${r.updated ? "갱신" : "임포트"} 완료: ${r.name} (${r.kind})`);
923
+ out(`${r.updated ? "Updated" : "Imported"}: ${r.name} (${r.kind})`);
885
924
  out(` slug: ${r.slug}`);
886
925
  out(` runtime: ${r.runtime} [${r.labels.join(", ")}]`);
887
926
  out(` path: ${r.path}`);
888
- if (r.firmSlug) out(` firm: ${r.firmSlug} (FIRMS 등록됨 사이드바 + 'agentlas firm ${r.firmSlug}')`);
927
+ if (r.firmSlug) out(` firm: ${r.firmSlug} (registered in Firms Desktop sidebar + 'agentlas firm ${r.firmSlug}')`);
889
928
  out("");
890
- out(`실행: agentlas ${r.slug} "..." · agentlas run ${r.slug} "..." (대상 프로젝트 폴더에서 실행)`);
929
+ out(`Run: agentlas ${r.slug} "..." · agentlas run ${r.slug} "..." (run from the target project folder)`);
891
930
  }
892
931
 
893
932
  // ── Agentlas Cloud packaging / marketplace ────────────────────────────────
@@ -963,7 +1002,7 @@ function hubTimeoutError(kind, ms) {
963
1002
  /** Hub/Cloud fetch + body reader. Headers 전 connect, chunk 사이 idle, 전 구간 total timeout. */
964
1003
  async function fetchHubCli(url, init = {}, options = {}) {
965
1004
  const fetchImpl = options.fetch || globalThis.fetch;
966
- if (typeof fetchImpl !== "function") throw new Error(" 런타임에 fetch가 없습니다.");
1005
+ if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime.");
967
1006
  const timeout = options.timeoutConfig ? directHubTimeoutConfig(options.timeoutConfig) : hubTimeoutConfig(options.env || process.env);
968
1007
  const controller = new AbortController();
969
1008
  const upstreamSignal = init.signal;
@@ -1027,7 +1066,7 @@ async function fetchHubCli(url, init = {}, options = {}) {
1027
1066
  } else {
1028
1067
  const raw = Buffer.from(await Promise.race([response.arrayBuffer(), terminal]));
1029
1068
  bytes = raw.length;
1030
- if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub 응답이 허용 크기(${HUB_RESPONSE_MAX_BYTES} bytes)를 초과했습니다.`);
1069
+ if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub response exceeds the allowed size (${HUB_RESPONSE_MAX_BYTES} bytes).`);
1031
1070
  chunks.push(raw);
1032
1071
  }
1033
1072
  if (idleTimer) clearTimeout(idleTimer);
@@ -1052,7 +1091,7 @@ function parseHubJsonCli(response, label) {
1052
1091
  try {
1053
1092
  return JSON.parse(response.text || "null");
1054
1093
  } catch {
1055
- throw new Error(`${label} 응답 JSON 형식이 올바르지 않습니다.`);
1094
+ throw new Error(`${label} returned invalid JSON.`);
1056
1095
  }
1057
1096
  }
1058
1097
 
@@ -1126,6 +1165,7 @@ async function cmdCloud(db, args, runtimeOverride) {
1126
1165
  " list [--json] list packages in your private Agent Cloud",
1127
1166
  " restore <slug> [--json] restore an owned Cloud package on this machine",
1128
1167
  " install <slug> compatibility alias: install from the public Hub",
1168
+ " plugin add <slug> install a Hub plugin (MCP servers)",
1129
1169
  " delete <slug> [--scope owner-private|hub-public] [--json]",
1130
1170
  " conditionally delete one exact observed Cloud revision",
1131
1171
  " search \"<what you need>\" [--limit 10]",
@@ -1144,7 +1184,7 @@ async function cmdCloud(db, args, runtimeOverride) {
1144
1184
  const result = await listOwnedCloudAgentsCli(Number(flags.limit || 100));
1145
1185
  if (flags.json) return out(JSON.stringify(result, null, 2));
1146
1186
  const agents = Array.isArray(result.results) ? result.results : [];
1147
- if (!agents.length) return out("Private Agent Cloud에 저장된 에이전트가 없습니다.");
1187
+ if (!agents.length) return out("No agents are stored in Private Agent Cloud.");
1148
1188
  for (const agent of agents) out(`${agent.slug}\t${agent.name || agent.nameEn || agent.slug}\t${agent.entityKind || "agent"}`);
1149
1189
  return;
1150
1190
  }
@@ -1240,8 +1280,8 @@ async function cmdCloud(db, args, runtimeOverride) {
1240
1280
  async function packageCloudAgentCli(db, root, opts) {
1241
1281
  const requestedRoot = path.resolve(root);
1242
1282
  let st;
1243
- try { st = fs.lstatSync(requestedRoot); } catch { throw new Error(`폴더를 찾을 없습니다: ${root}`); }
1244
- if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(`실제 폴더가 아닙니다: ${root}`);
1283
+ try { st = fs.lstatSync(requestedRoot); } catch { throw new Error(`Folder not found: ${root}`); }
1284
+ if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(`Not a real directory: ${root}`);
1245
1285
  const rootPath = fs.realpathSync.native(requestedRoot);
1246
1286
  const visibility = opts.visibility || "private-link";
1247
1287
  const isPublicHubPublish = visibility === "marketplace";
@@ -1940,19 +1980,19 @@ function cloudCasResponseErrorCli(response, label) {
1940
1980
  let body = null;
1941
1981
  try { body = JSON.parse(response.text || "null"); } catch { /* generic below */ }
1942
1982
  const code = body && typeof body.code === "string" ? body.code : "cloud_request_failed";
1943
- let message = `${label} 실패 ${response.status}`;
1983
+ let message = `${label} failed with HTTP ${response.status}`;
1944
1984
  if (response.status === 412 && code === "cloud_agent_revision_conflict") {
1945
1985
  const current = body && body.current ? body.current : body && body.conflict && body.conflict.current;
1946
1986
  message = current
1947
1987
  ? `다른 PC에서 이 Agent Cloud 자산이 변경되었습니다. 자동 덮어쓰기는 중단했습니다. \`agentlas cloud list\`로 최신 revision을 확인하고 \`agentlas cloud restore ${current.slug || "<slug>"}\`로 복원한 뒤 변경 사항을 병합하세요.`
1948
1988
  : "이 Agent Cloud 자산은 다른 PC에서 삭제되었거나 다른 식별자로 다시 생성되었습니다. 자동 재생성은 중단했습니다. `agentlas cloud list`로 현재 상태를 확인하세요.";
1949
1989
  } else if (response.status === 428 && code === "client_upgrade_required") {
1950
- message = "기존 Cloud 자산을 안전하게 갱신할 base revision이 없습니다. 서버 revision 자동 복사하지 않습니다. `agentlas cloud list`로 확인하고 `agentlas cloud restore <slug>`로 복원한 다시 저장하세요.";
1990
+ message = "No base revision is available to safely update the existing Cloud asset. The server revision will not be copied automatically. Check `agentlas cloud list`, restore with `agentlas cloud restore <slug>`, then save again.";
1951
1991
  } else if (response.status === 503 && code === "cloud_mutations_maintenance") {
1952
1992
  const retryAfter = response.headers && typeof response.headers.get === "function" ? response.headers.get("retry-after") : null;
1953
- message = `Agent Cloud 저장/삭제가 잠시 점검 중입니다${retryAfter ? ` ( ${retryAfter} 후 재시도)` : ""}. 읽기·목록·복원은 계속 사용할 있습니다.`;
1993
+ message = `Agent Cloud save/delete is temporarily under maintenance${retryAfter ? ` (retry in about ${retryAfter} seconds)` : ""}. Read, list, and restore remain available.`;
1954
1994
  } else if (body && typeof body.error === "string") {
1955
- message = `${label} 실패 ${response.status}: ${body.error.slice(0, 300)}`;
1995
+ message = `${label} failed with HTTP ${response.status}: ${body.error.slice(0, 300)}`;
1956
1996
  }
1957
1997
  const error = new Error(message);
1958
1998
  error.code = code;
@@ -1964,8 +2004,8 @@ function cloudCasResponseErrorCli(response, label) {
1964
2004
 
1965
2005
  async function registerCloudAgentCli(manifest, bundlePath, review, visibility, options = {}) {
1966
2006
  const cookie = await cloudSessionCookieCli();
1967
- if (!cookie) fail("agentlas.cloud 로그인이 필요합니다. 데스크톱 앱에서 로그인하거나 AGENTLAS_SESSION을 설정하세요.");
1968
- if (typeof fetch !== "function") fail(" 런타임에 fetch가 없습니다( 런타임으로 실행 필요).");
2007
+ if (!cookie) fail("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
2008
+ if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
1969
2009
  const base = (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
1970
2010
  const bundle = JSON.parse(fs.readFileSync(bundlePath, "utf8"));
1971
2011
  const expectedScope = cloudScopeForVisibility(visibility);
@@ -2038,8 +2078,8 @@ async function deleteCloudAgentCli(slug, options = {}) {
2038
2078
  const safeSlug = String(slug || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
2039
2079
  if (!safeSlug) fail("usage: agentlas cloud delete <slug> [--json]");
2040
2080
  const cookie = await cloudSessionCookieCli();
2041
- if (!cookie) fail("agentlas.cloud 로그인이 필요합니다. 데스크톱 앱에서 로그인하거나 AGENTLAS_SESSION을 설정하세요.");
2042
- if (typeof fetch !== "function") fail(" 런타임에 fetch가 없습니다( 런타임으로 실행 필요).");
2081
+ if (!cookie) fail("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
2082
+ if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
2043
2083
  const scope = options.scope == null ? null : normalizeCloudScopeFlagCli(options.scope);
2044
2084
  if (options.scope != null && !scope) throw new Error("--scope must be owner-private or hub-public");
2045
2085
  const localEntry = findCloudAssetDescriptorCli(safeSlug, scope);
@@ -2142,28 +2182,121 @@ async function cloudSessionCookieCli() {
2142
2182
  async function cmdCloudInstall(db, slug) {
2143
2183
  if (!slug) fail("usage: agentlas cloud install <slug>");
2144
2184
  const listing = await fetchCloudManifestCli(slug);
2145
- if (!listing) fail(`Hub agent 찾을 없습니다: ${slug}`);
2185
+ if (!listing) fail(`Hub agent not found: ${slug}`);
2146
2186
  if (listing.delivery && listing.delivery.mode === "call_only") {
2147
- fail(`이 Hub 에이전트는 소스 설치가 허용되지 않은 call-only 자산입니다. 실행: agentlas call ${slug}`);
2187
+ fail(`This Hub agent is call-only and cannot be installed from source. Run: agentlas call ${slug}`);
2148
2188
  }
2149
2189
  const agent = persistCloudListingCli(db, listing);
2150
2190
  out(`✓ Hub installed ${agent.slug} — ${agent.name}`);
2151
2191
  if (agent.localPath) out(` files: ${agent.localPath}`);
2152
2192
  }
2153
2193
 
2194
+ // ── Hub 플러그인 설치 ────────────────────────────────────────────────────────
2195
+ // 서버는 처음부터 준비돼 있었다: /api/plugins/<slug>가 agentlas.plugin/v1 매니페스트를 주고,
2196
+ // 그 라우트 주석이 이 CLI(`agentlas plugin add <slug>`)를 소비자로 지목한다. 그런데 이 명령이
2197
+ // 구현된 적이 없어서, 카탈로그 146개가 전부 "존재하지 않는 설치 명령"을 광고하고 있었다.
2198
+ // (`agentlas install`은 marketplace.get_manifest{kind:"agent"} 고정이라 플러그인엔 안 먹는다.)
2199
+ async function fetchPluginManifestCli(slug) {
2200
+ const base = (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
2201
+ const resp = await fetchHubCli(`${base}/api/plugins/${encodeURIComponent(slug)}`, {
2202
+ headers: { accept: "application/json" },
2203
+ });
2204
+ if (resp.status === 404) return null;
2205
+ if (!resp.ok) fail(`plugin lookup failed with HTTP ${resp.status}`);
2206
+ const manifest = parseHubJsonCli(resp, "plugin manifest");
2207
+ if (!manifest || manifest.schema !== "agentlas.plugin/v1") {
2208
+ fail(`Unexpected plugin manifest schema for ${slug}.`);
2209
+ }
2210
+ return manifest;
2211
+ }
2212
+
2213
+ /** 매니페스트의 mcp[] 항목을 mcp_servers 행으로 정규화. stdio(command)와 remote(url)를 구분한다. */
2214
+ function pluginMcpRowCli(slug, entry, index) {
2215
+ const source = typeof entry?.source === "string" ? entry.source.trim() : "";
2216
+ const name = (typeof entry?.name === "string" && entry.name.trim()) || `${slug}-${index + 1}`;
2217
+ const remote = /^https?:\/\//i.test(source);
2218
+ // 원격은 URL, stdio는 실행 커맨드다. 둘을 섞으면 codex config.toml 스키마 위반으로
2219
+ // 런타임이 통째로 죽는다(Runtime Doctor가 반복해서 잡던 사고 계열).
2220
+ if (!remote && !source) return null;
2221
+ const argv = remote ? [] : source.split(/\s+/).filter(Boolean);
2222
+ return {
2223
+ id: require("node:crypto").randomUUID(),
2224
+ catalogId: `hub:${slug}:${name}`,
2225
+ name,
2226
+ transport: remote ? "http" : "stdio",
2227
+ command: remote ? null : (argv[0] ?? null),
2228
+ argsJson: JSON.stringify(remote ? [] : argv.slice(1)),
2229
+ url: remote ? source : null,
2230
+ envKeysJson: JSON.stringify(
2231
+ Array.isArray(entry?.envKeys) ? entry.envKeys.filter((key) => typeof key === "string") : [],
2232
+ ),
2233
+ };
2234
+ }
2235
+
2236
+ async function cmdPluginAdd(db, slug) {
2237
+ if (!slug) fail('usage: agentlas plugin add <slug> (run agentlas plugin list first)');
2238
+ const manifest = await fetchPluginManifestCli(slug);
2239
+ if (!manifest) fail(`Hub plugin not found: ${slug}`);
2240
+ const entries = Array.isArray(manifest.mcp) ? manifest.mcp : [];
2241
+ const rows = entries.map((entry, index) => pluginMcpRowCli(slug, entry, index)).filter(Boolean);
2242
+ if (!rows.length) {
2243
+ // 설치할 MCP 서버가 없으면 조용히 성공했다고 하지 않는다 — 사용자는 이 플러그인이
2244
+ // 붙었다고 믿고 도구를 기대하게 된다.
2245
+ fail(
2246
+ `${slug} ships no MCP server to install (skills-only or source-link plugin). ` +
2247
+ `Nothing was registered. See: ${manifest.source?.repo || manifest.source?.homepage || "the plugin page"}`,
2248
+ );
2249
+ }
2250
+ let installed = 0;
2251
+ let reused = 0;
2252
+ for (const row of rows) {
2253
+ const existing = db.prepare("SELECT id FROM mcp_servers WHERE catalog_id = ? LIMIT 1").get(row.catalogId);
2254
+ if (existing) { reused += 1; continue; } // 멱등: 재설치가 중복 행을 만들지 않는다
2255
+ db.prepare(
2256
+ `INSERT INTO mcp_servers (id, catalog_id, name, name_en, transport, command, args_json, url, env_keys_json, enabled, installed_at)
2257
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
2258
+ ).run(
2259
+ row.id, row.catalogId, row.name, row.name, row.transport,
2260
+ row.command, row.argsJson, row.url, row.envKeysJson, new Date().toISOString(),
2261
+ );
2262
+ installed += 1;
2263
+ }
2264
+ out(`✓ Plugin installed ${manifest.slug} — ${manifest.name}`);
2265
+ out(` MCP servers: ${installed} added${reused ? `, ${reused} already present` : ""}`);
2266
+ const authKind = manifest.auth?.kind;
2267
+ if (authKind && authKind !== "none") {
2268
+ out(` ⚠ Requires ${authKind} — set credentials before use (agentlas creds).`);
2269
+ }
2270
+ if (Array.isArray(manifest.skills) && manifest.skills.length) {
2271
+ out(` skills declared: ${manifest.skills.map((skill) => skill.name).filter(Boolean).join(", ")}`);
2272
+ }
2273
+ out(" Only full-access turns wire active stdio servers into the runtime (agentlas mcp).");
2274
+ }
2275
+
2276
+ async function cmdPluginList() {
2277
+ const result = await callAgentlasMcpToolCli("marketplace.list_plugins", {});
2278
+ const plugins = (result && (result.plugins || result.results)) || [];
2279
+ if (!plugins.length) return out("No Hub plugins are available.");
2280
+ for (const plugin of plugins.slice(0, 60)) {
2281
+ out(`${String(plugin.slug || "").padEnd(32).slice(0, 32)} ${String(plugin.name || "").slice(0, 44)}`);
2282
+ }
2283
+ out("");
2284
+ out("Install: agentlas plugin add <slug>");
2285
+ }
2286
+
2154
2287
  async function callAgentlasMcpToolCli(name, args, { requireSession = false } = {}) {
2155
- if (typeof fetch !== "function") fail(" 런타임에 fetch가 없습니다( 런타임으로 실행 필요).");
2288
+ if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
2156
2289
  const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
2157
2290
  const headers = { "content-type": "application/json" };
2158
2291
  const cookie = await cloudSessionCookieCli();
2159
- if (requireSession && !cookie) fail("Agent Cloud에는 로그인이 필요합니다. 먼저 `agentlas login`을 실행하세요.");
2292
+ if (requireSession && !cookie) fail("Agent Cloud sign-in is required. Run `agentlas login` first.");
2160
2293
  if (cookie) headers.cookie = cookie;
2161
2294
  const resp = await fetchHubCli(`${base.replace(/\/$/, "")}/tools/call`, {
2162
2295
  method: "POST",
2163
2296
  headers,
2164
2297
  body: JSON.stringify({ method: name, params: { name, arguments: args || {} } }),
2165
2298
  });
2166
- if (!resp.ok) fail(`${name} 실패 ${resp.status}`);
2299
+ if (!resp.ok) fail(`${name} failed with HTTP ${resp.status}`);
2167
2300
  const json = parseHubJsonCli(resp, name);
2168
2301
  if (json.error) fail(`${name}: ${json.error.message || "unknown error"}`);
2169
2302
  return json.result || null;
@@ -7643,38 +7776,320 @@ function contextLine(json) {
7643
7776
  return "";
7644
7777
  }
7645
7778
  }
7646
- // 작업 폴더 반복 방문 → 활성화(.agentlas 생성). 앱의 activation.ts와 동일한 정책(2회).
7647
- function recordCliFolderVisit(db, projectPath) {
7648
- if (!tableExists(db, "folder_activity")) return { activated: false };
7649
- const now = new Date().toISOString();
7779
+ const AGENTLAS_PROJECT_STATE_IGNORE_START = "# >>> agentlas local project state >>>";
7780
+ const AGENTLAS_PROJECT_STATE_IGNORE_END = "# <<< agentlas local project state <<<";
7781
+ const AGENTLAS_GITIGNORE_MAX_BYTES = 1024 * 1024;
7782
+ const projectBootstrapStates = new Map();
7783
+
7784
+ function terminalProjectCandidateCli(projectPath) {
7650
7785
  try {
7651
- const row = db.prepare("SELECT visits, activated_at FROM folder_activity WHERE path=?").get(projectPath);
7652
- let visits, activatedAt;
7653
- if (row) {
7654
- visits = row.visits + 1; activatedAt = row.activated_at;
7655
- db.prepare("UPDATE folder_activity SET visits=?, last_seen=? WHERE path=?").run(visits, now, projectPath);
7656
- } else {
7657
- visits = 1; activatedAt = null;
7658
- db.prepare("INSERT INTO folder_activity (path, visits, activated_at, first_seen, last_seen) VALUES (?,?,NULL,?,?)").run(projectPath, visits, now, now);
7786
+ const root = path.resolve(projectPath || process.cwd());
7787
+ const unsafe = new Set([
7788
+ path.resolve(os.homedir()),
7789
+ path.parse(root).root,
7790
+ path.resolve(userDataDir()),
7791
+ path.resolve(runCwd()),
7792
+ ]);
7793
+ if (unsafe.has(root)) return null;
7794
+ const stat = fs.statSync(root);
7795
+ if (!stat.isDirectory()) return null;
7796
+ return root;
7797
+ } catch {
7798
+ return null;
7799
+ }
7800
+ }
7801
+
7802
+ function assertNoSymlinkInAgentlasStateCli(stateDir) {
7803
+ const pending = [stateDir];
7804
+ let visited = 0;
7805
+ while (pending.length && visited < 4096) {
7806
+ const current = pending.pop();
7807
+ visited += 1;
7808
+ let stat;
7809
+ try { stat = fs.lstatSync(current); } catch (error) {
7810
+ if (error && error.code === "ENOENT") continue;
7811
+ throw error;
7659
7812
  }
7660
- if (!activatedAt && visits >= 2) {
7661
- db.prepare("UPDATE folder_activity SET activated_at=? WHERE path=?").run(now, projectPath);
7662
- ensureProjectMemoryCli(projectPath);
7663
- activatedAt = now;
7813
+ if (stat.isSymbolicLink()) throw new Error(".agentlas local state must not contain symbolic links");
7814
+ if (stat.isDirectory()) {
7815
+ for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
7664
7816
  }
7665
- return { activated: !!activatedAt };
7666
- } catch { return { activated: false }; }
7817
+ }
7818
+ if (pending.length) throw new Error(".agentlas local state exceeds the safe bootstrap inspection limit");
7667
7819
  }
7668
- // `agentlas run` 등이 호출된 작업 디렉터리 → 활성 프로젝트 경로(또는 null).
7669
- function activeProjectPath(db) {
7820
+
7821
+ function readRegularUtf8FileNoFollowCli(filePath, maxBytes = AGENTLAS_GITIGNORE_MAX_BYTES) {
7822
+ let before;
7823
+ try { before = fs.lstatSync(filePath); } catch (error) {
7824
+ if (error && error.code === "ENOENT") return { exists: false, content: "", mode: 0o644, stat: null };
7825
+ throw error;
7826
+ }
7827
+ if (before.isSymbolicLink() || !before.isFile()) throw new Error(".gitignore must be a regular non-symbolic-link file");
7828
+ if (before.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
7829
+
7830
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
7831
+ let fd;
7670
7832
  try {
7671
- const cwd = process.cwd();
7672
- if (cwd === os.homedir() || cwd === userDataDir() || cwd === runCwd()) return null;
7673
- const v = recordCliFolderVisit(db, cwd);
7674
- return v.activated ? cwd : null;
7675
- } catch { return null; }
7833
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
7834
+ } catch (error) {
7835
+ if (process.platform !== "win32" || !noFollow || !["EINVAL", "ENOTSUP"].includes(error && error.code)) throw error;
7836
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY);
7837
+ }
7838
+ try {
7839
+ const opened = fs.fstatSync(fd);
7840
+ if (!opened.isFile()) throw new Error(".gitignore changed type during bootstrap");
7841
+ if (opened.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
7842
+ if (
7843
+ Number.isFinite(before.dev) && Number.isFinite(before.ino) &&
7844
+ (before.dev !== opened.dev || before.ino !== opened.ino)
7845
+ ) {
7846
+ throw new Error(".gitignore changed during bootstrap");
7847
+ }
7848
+ const chunks = [];
7849
+ let total = 0;
7850
+ while (total <= maxBytes) {
7851
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
7852
+ const count = fs.readSync(fd, buffer, 0, buffer.length, null);
7853
+ if (!count) break;
7854
+ chunks.push(buffer.subarray(0, count));
7855
+ total += count;
7856
+ }
7857
+ if (total > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
7858
+ const after = fs.fstatSync(fd);
7859
+ if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) throw new Error(".gitignore changed while it was being read");
7860
+ let content;
7861
+ try { content = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total)); } catch {
7862
+ throw new Error(".gitignore must contain valid UTF-8 text");
7863
+ }
7864
+ return { exists: true, content, mode: before.mode & 0o777, stat: before };
7865
+ } finally {
7866
+ fs.closeSync(fd);
7867
+ }
7868
+ }
7869
+
7870
+ function assertFileSnapshotUnchangedCli(filePath, snapshot) {
7871
+ if (!snapshot.exists) {
7872
+ try {
7873
+ fs.lstatSync(filePath);
7874
+ throw new Error(".gitignore appeared during bootstrap");
7875
+ } catch (error) {
7876
+ if (error && error.code === "ENOENT") return;
7877
+ throw error;
7878
+ }
7879
+ }
7880
+ const current = fs.lstatSync(filePath);
7881
+ if (current.isSymbolicLink() || !current.isFile()) throw new Error(".gitignore changed type during bootstrap");
7882
+ const original = snapshot.stat;
7883
+ if (
7884
+ !original || current.dev !== original.dev || current.ino !== original.ino ||
7885
+ current.size !== original.size || current.mtimeMs !== original.mtimeMs
7886
+ ) {
7887
+ throw new Error(".gitignore changed during bootstrap");
7888
+ }
7889
+ }
7890
+
7891
+ function replaceRegularFileCli(tempPath, destinationPath, snapshot) {
7892
+ try {
7893
+ fs.renameSync(tempPath, destinationPath);
7894
+ return;
7895
+ } catch (error) {
7896
+ if (process.platform !== "win32" || !snapshot.exists || !["EEXIST", "EPERM", "EACCES"].includes(error && error.code)) {
7897
+ throw error;
7898
+ }
7899
+ }
7900
+
7901
+ // Windows can reject replacement of an existing file. Keep a same-directory
7902
+ // rollback copy so an interrupted replacement never silently loses user rules.
7903
+ assertFileSnapshotUnchangedCli(destinationPath, snapshot);
7904
+ const backup = `${destinationPath}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
7905
+ fs.renameSync(destinationPath, backup);
7906
+ try {
7907
+ fs.renameSync(tempPath, destinationPath);
7908
+ } catch (error) {
7909
+ try {
7910
+ if (!fs.existsSync(destinationPath)) fs.renameSync(backup, destinationPath);
7911
+ } catch { /* preserve the original error and leave the backup recoverable */ }
7912
+ throw error;
7913
+ }
7914
+ try { fs.unlinkSync(backup); } catch { /* a harmless rollback copy may remain on locked Windows hosts */ }
7915
+ }
7916
+
7917
+ function ensureAgentlasProjectStateIgnoreCli(projectPath) {
7918
+ const root = terminalProjectCandidateCli(projectPath);
7919
+ if (!root) throw new Error("refusing to initialize an unsafe Agentlas project root");
7920
+ const stateDir = path.join(root, ".agentlas");
7921
+ let stateExists = false;
7922
+ try {
7923
+ const state = fs.lstatSync(stateDir);
7924
+ stateExists = true;
7925
+ if (state.isSymbolicLink() || !state.isDirectory()) throw new Error(".agentlas must be a real directory");
7926
+ assertNoSymlinkInAgentlasStateCli(stateDir);
7927
+ } catch (error) {
7928
+ if (error && error.code !== "ENOENT") throw error;
7929
+ }
7930
+
7931
+ const gitignorePath = path.join(root, ".gitignore");
7932
+ const snapshot = readRegularUtf8FileNoFollowCli(gitignorePath);
7933
+ const existing = snapshot.content;
7934
+ const mode = snapshot.mode || 0o644;
7935
+
7936
+ let next = existing;
7937
+ const start = existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_START);
7938
+ const end = start >= 0 ? existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_END, start) : -1;
7939
+ if (start >= 0 && end >= 0) {
7940
+ const blockEnd = end + AGENTLAS_PROJECT_STATE_IGNORE_END.length;
7941
+ const block = existing.slice(start, blockEnd);
7942
+ if (!/^\.agentlas\/$/m.test(block)) {
7943
+ next = `${existing.slice(0, start)}${block.replace(AGENTLAS_PROJECT_STATE_IGNORE_START, `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/`)}${existing.slice(blockEnd)}`;
7944
+ }
7945
+ } else {
7946
+ const block = `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/\n${AGENTLAS_PROJECT_STATE_IGNORE_END}\n`;
7947
+ next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : block;
7948
+ }
7949
+ if (next !== existing) {
7950
+ const temp = path.join(root, `.gitignore.agentlas-${process.pid}-${crypto.randomUUID()}.tmp`);
7951
+ fs.writeFileSync(temp, next.endsWith("\n") ? next : `${next}\n`, { encoding: "utf8", mode, flag: "wx" });
7952
+ try {
7953
+ assertFileSnapshotUnchangedCli(gitignorePath, snapshot);
7954
+ replaceRegularFileCli(temp, gitignorePath, snapshot);
7955
+ } catch (error) {
7956
+ try { fs.unlinkSync(temp); } catch { /* ignore */ }
7957
+ throw error;
7958
+ }
7959
+ }
7960
+ if (!stateExists) fs.mkdirSync(stateDir, { recursive: false, mode: 0o700 });
7961
+ assertNoSymlinkInAgentlasStateCli(stateDir);
7962
+ try { fs.chmodSync(stateDir, 0o700); } catch { /* Windows/best effort */ }
7963
+ }
7964
+
7965
+ function hardenAgentlasProjectStateCli(projectPath) {
7966
+ const root = terminalProjectCandidateCli(projectPath);
7967
+ if (!root) return;
7968
+ const stateDir = path.join(root, ".agentlas");
7969
+ const pending = [stateDir];
7970
+ let visited = 0;
7971
+ while (pending.length && visited < 4096) {
7972
+ const current = pending.pop();
7973
+ visited += 1;
7974
+ try {
7975
+ const stat = fs.lstatSync(current);
7976
+ if (stat.isSymbolicLink()) continue;
7977
+ if (stat.isDirectory()) {
7978
+ try { fs.chmodSync(current, 0o700); } catch { /* Windows/best effort */ }
7979
+ for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
7980
+ } else if (stat.isFile()) {
7981
+ try { fs.chmodSync(current, 0o600); } catch { /* Windows/best effort */ }
7982
+ }
7983
+ } catch { /* disappearing files and ACL-only hosts are best effort */ }
7984
+ }
7985
+ }
7986
+
7987
+ function ensureCoreProjectCli(projectPath, options = {}) {
7988
+ const root = terminalProjectCandidateCli(projectPath);
7989
+ if (!root) throw new Error("Agentlas project bootstrap requires a real project directory");
7990
+ fs.accessSync(root, fs.constants.R_OK | fs.constants.W_OK);
7991
+ ensureAgentlasProjectStateIgnoreCli(root);
7992
+ const cached = projectBootstrapStates.get(root);
7993
+ if (cached && fs.existsSync(path.join(root, ".agentlas", "project-soul-memory.md"))) {
7994
+ return cached === "core";
7995
+ }
7996
+ projectBootstrapStates.delete(root);
7997
+ const coreRoot = resolveCoreRuntimeRoot(options.coreRoot);
7998
+ const hasCanonicalBootstrap = Boolean(
7999
+ coreRoot && fs.existsSync(path.join(coreRoot, "agentlas_cloud", "project_bootstrap.py")),
8000
+ );
8001
+ if (hasCanonicalBootstrap) {
8002
+ const result = captureCoreJsonSync(
8003
+ "agentlas_cloud",
8004
+ ["project", "ensure", "--project", root, "--reason", options.reason || "terminal-first-contact"],
8005
+ { cwd: root },
8006
+ coreRoot,
8007
+ );
8008
+ const canonical = Boolean(
8009
+ result
8010
+ && result.schemaVersion === "agentlas.project-bootstrap.v1"
8011
+ && ["active", "privacy_warning"].includes(result.status)
8012
+ && result.mergeOnly === true
8013
+ && result.privacyBlockInstalled === true
8014
+ && result.privateModeCompliant === true
8015
+ && Array.isArray(result.missing)
8016
+ && result.missing.length === 0
8017
+ && Array.isArray(result.overwritten)
8018
+ && result.overwritten.length === 0
8019
+ && Array.isArray(result.permissionIssues)
8020
+ && result.permissionIssues.length === 0
8021
+ );
8022
+ if (canonical) {
8023
+ // Core owns the canonical seed. Terminal adds one intentionally broader
8024
+ // guard so future local memory files are private without a release update.
8025
+ ensureAgentlasProjectStateIgnoreCli(root);
8026
+ hardenAgentlasProjectStateCli(root);
8027
+ projectBootstrapStates.set(root, "core");
8028
+ return true;
8029
+ }
8030
+ throw new Error("Agentlas Core returned an incomplete project bootstrap contract");
8031
+ }
8032
+ // A just-updated Terminal can briefly see the previous Core. The legacy
8033
+ // merge-only seed remains local-only and Core is retried next process.
8034
+ ensureProjectMemoryCli(root);
8035
+ if (!fs.existsSync(path.join(root, ".agentlas"))) {
8036
+ throw new Error("Agentlas project bootstrap could not create private local state");
8037
+ }
8038
+ ensureAgentlasProjectStateIgnoreCli(root);
8039
+ hardenAgentlasProjectStateCli(root);
8040
+ projectBootstrapStates.set(root, "fallback");
8041
+ return false;
8042
+ }
8043
+
8044
+ // Passive checks never increment visits or touch the project. Activation is
8045
+ // reserved for an actual write/full Terminal execution or an explicit ensure.
8046
+ function recordCliFolderVisit(db, projectPath, options = {}) {
8047
+ const root = terminalProjectCandidateCli(projectPath);
8048
+ if (!root) return { activated: false };
8049
+ const activate = options.activate === true;
8050
+ try {
8051
+ if (!activate) {
8052
+ const row = tableExists(db, "folder_activity")
8053
+ ? db.prepare("SELECT activated_at FROM folder_activity WHERE path=?").get(root)
8054
+ : null;
8055
+ return { activated: Boolean(row && row.activated_at) || fs.existsSync(path.join(root, ".agentlas")) };
8056
+ }
8057
+
8058
+ ensureCoreProjectCli(root, { reason: options.reason || "terminal-first-contact", coreRoot: options.coreRoot });
8059
+ if (!tableExists(db, "folder_activity")) return { activated: true };
8060
+ const now = new Date().toISOString();
8061
+ const row = db.prepare("SELECT visits FROM folder_activity WHERE path=?").get(root);
8062
+ if (row) {
8063
+ db.prepare("UPDATE folder_activity SET visits=?, activated_at=COALESCE(activated_at,?), last_seen=? WHERE path=?")
8064
+ .run(Number(row.visits || 0) + 1, now, now, root);
8065
+ } else {
8066
+ db.prepare("INSERT INTO folder_activity (path, visits, activated_at, first_seen, last_seen) VALUES (?,?,?,?,?)")
8067
+ .run(root, 1, now, now, now);
8068
+ }
8069
+ return { activated: true };
8070
+ } catch (error) {
8071
+ // An activation failure can mean that the project-local privacy boundary
8072
+ // could not be established (for example, a symlinked or oversized
8073
+ // .gitignore). Never continue a write/full execution in that state.
8074
+ if (activate) throw error;
8075
+ return { activated: false };
8076
+ }
7676
8077
  }
7677
- function cliMemoryContext(db, projectPath) {
8078
+
8079
+ function activeProjectPath(db, options = {}) {
8080
+ const root = terminalProjectCandidateCli(options.projectPath || process.cwd());
8081
+ if (!root) return null;
8082
+ const result = recordCliFolderVisit(db, root, options);
8083
+ return result.activated ? root : null;
8084
+ }
8085
+
8086
+ function ensureTerminalProjectForExecutionCli(db, projectPath, permission = PERMISSION, reason = "terminal-first-contact") {
8087
+ const root = terminalProjectCandidateCli(projectPath);
8088
+ if (!root) return null;
8089
+ if (permission === "read") return activeProjectPath(db, { projectPath: root });
8090
+ return activeProjectPath(db, { projectPath: root, activate: true, reason });
8091
+ }
8092
+ function cliMemoryContext(db, projectPath, agentId = null) {
7678
8093
  const sections = [];
7679
8094
  const arch = loadArch();
7680
8095
  ensureMemoryContextColumn(db);
@@ -7690,14 +8105,49 @@ function cliMemoryContext(db, projectPath) {
7690
8105
  }
7691
8106
  if (tableExists(db, "memory_entries")) {
7692
8107
  try {
7693
- const rows = projectPath
7694
- ? db.prepare("SELECT kind, content, context_json FROM memory_entries WHERE superseded_at IS NULL AND scope!='session' AND (project_path=? OR (project_path IS NULL AND scope IN ('user_identity','team_memory','agent_team'))) ORDER BY created_at DESC LIMIT 12").all(projectPath)
7695
- : db.prepare("SELECT kind, content, context_json FROM memory_entries WHERE project_path IS NULL AND scope!='session' AND superseded_at IS NULL ORDER BY created_at DESC LIMIT 12").all();
7696
- if (rows.length) sections.push((projectPath ? "### Recent curated memory\n" : "### Curated memory (global)\n") + rows.map((r) => `- [${r.kind}] ${r.content}${contextLine(r.context_json)}`).join("\n"));
8108
+ // New writes are read through a scoped global<->project timeline. The
8109
+ // project key is a digest, and team/agent lanes additionally require the
8110
+ // current owner id, so project B cannot recall project A's local memory.
8111
+ const governed = terminalMemoryGovernance.listScopedTimeline(db, {
8112
+ projectPath,
8113
+ agentId,
8114
+ limit: 16,
8115
+ });
8116
+ const seen = new Set(governed.map((row) => row.id));
8117
+ // Legacy rows predate the timeline. Keep only intentional user-global
8118
+ // rows, this exact project, and this exact agent/team owner. In
8119
+ // particular, do not revive the old global team-memory leakage query.
8120
+ const legacy = projectPath
8121
+ ? db.prepare(`
8122
+ SELECT id,kind,content,context_json,created_at
8123
+ FROM memory_entries
8124
+ WHERE superseded_at IS NULL AND (
8125
+ (scope='user_identity' AND project_path IS NULL)
8126
+ OR (scope='project' AND project_path=?)
8127
+ OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND (project_path IS NULL OR project_path=?))
8128
+ )
8129
+ ORDER BY created_at DESC LIMIT 16
8130
+ `).all(projectPath, agentId, projectPath)
8131
+ : db.prepare(`
8132
+ SELECT id,kind,content,context_json,created_at
8133
+ FROM memory_entries
8134
+ WHERE superseded_at IS NULL AND (
8135
+ (scope='user_identity' AND project_path IS NULL)
8136
+ OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND project_path IS NULL)
8137
+ )
8138
+ ORDER BY created_at DESC LIMIT 16
8139
+ `).all(agentId);
8140
+ const rows = [...governed, ...legacy.filter((row) => !seen.has(row.id))].slice(0, 16);
8141
+ if (rows.length) {
8142
+ sections.push(
8143
+ (projectPath ? "### Scoped global + current-project memory timeline\n" : "### Curated user-global memory\n") +
8144
+ rows.map((r) => `- [${r.kind}] ${r.content}${contextLine(r.context_json)}`).join("\n"),
8145
+ );
8146
+ }
7697
8147
  } catch { /* ignore */ }
7698
8148
  }
7699
8149
  if (!sections.length) return "";
7700
- return "## Agentlas memory (read before answering; five-scope + request_context recall)\n\n" + sections.join("\n\n");
8150
+ return "## Agentlas memory (read before answering; governed scope recall)\n\n" + sections.join("\n\n");
7701
8151
  }
7702
8152
  function parseMemoryEventsCli(text) {
7703
8153
  const heading = loadArch().eventsHeading;
@@ -7715,11 +8165,16 @@ function parseMemoryEventsCli(text) {
7715
8165
  function curateCliReply(db, text, ctx) {
7716
8166
  const { events, cleaned } = parseMemoryEventsCli(text);
7717
8167
  const style = require("./agentlas-style.cjs");
8168
+ if (ctx && ctx.permission === "read") return style.sanitizeAssistantText(cleaned);
7718
8169
  if (!events.length || !tableExists(db, "memory_entries")) return style.sanitizeAssistantText(cleaned);
7719
8170
  ensureMemoryContextColumn(db);
7720
8171
  const arch = loadArch();
7721
8172
  const { randomUUID } = require("node:crypto");
7722
8173
  const now = new Date().toISOString();
8174
+ const rememberCurated = (memory) => {
8175
+ if (!ctx || !Array.isArray(ctx.curatedMemories) || !memory) return;
8176
+ if (!ctx.curatedMemories.some((item) => item.id === memory.id)) ctx.curatedMemories.push(memory);
8177
+ };
7723
8178
  for (const ev of events) {
7724
8179
  const content = ev && typeof ev.content === "string" ? ev.content.trim() : "";
7725
8180
  if (!content) continue;
@@ -7735,9 +8190,16 @@ function curateCliReply(db, text, ctx) {
7735
8190
  const ppath = scope === "project" ? ctx.projectPath : null;
7736
8191
  const requestContext = normalizeRequestContext(ev, ctx, ppath);
7737
8192
  try {
7738
- const dup = db.prepare("SELECT 1 FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath);
7739
- if (dup) continue;
7740
- db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(randomUUID(), scope, kind, content, ctx.projectId || null, ppath, ctx.agentId || null, null, ev.confidence || "medium", ev.sensitivity || "internal", JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
8193
+ const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath);
8194
+ if (dup) {
8195
+ rememberCurated({ ...dup, requestContext });
8196
+ continue;
8197
+ }
8198
+ const memoryId = randomUUID();
8199
+ const confidence = ev.confidence || "medium";
8200
+ const sensitivity = ev.sensitivity || "internal";
8201
+ db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, ctx.agentId || null, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
8202
+ rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
7741
8203
  logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
7742
8204
  } catch { /* ignore */ }
7743
8205
  }
@@ -7756,7 +8218,50 @@ function prefsLang() {
7756
8218
  }
7757
8219
  }
7758
8220
 
7759
- function augmentSystem(db, baseSystem, ctx, withEmitter) {
8221
+ const TERMINAL_MEMORY_CORE_MAX_TOKENS = 150;
8222
+ const TERMINAL_MEMORY_CORE = [
8223
+ "## Memory governance",
8224
+ "End every completed reply with hidden `## Memory Events` plus fenced JSON:",
8225
+ '{"turn_id":"<stable-id>","observation":{"outcome":"completed","summary":"safe short outcome"},"candidates":[]}',
8226
+ "Candidates 0..N: memory_kind,content,suggested_scope,confidence.",
8227
+ "Scopes: user_global|team|agent|project|session|discard.",
8228
+ "No raw prompts/transcripts, secrets, logs, or absolute paths. Curator suggests; deterministic gates decide writes.",
8229
+ ].join("\n");
8230
+ const MEMORY_DETAIL_RE = /\b(?:remember|memory|save this|record this|memory event)\b|기억|메모리|저장해|기록해|남겨/i;
8231
+ const CREDENTIAL_INDEX_RE = /\b(?:deploy|release|billing|auth|oauth|credential|api key|secret key|cloud)\b|배포|릴리스|출시|결제|인증|자격 증명|API\s*키|시크릿|클라우드/i;
8232
+
8233
+ function approximatePromptTokens(text) {
8234
+ return Math.ceil(Buffer.byteLength(String(text || ""), "utf8") / 3);
8235
+ }
8236
+ if (approximatePromptTokens(TERMINAL_MEMORY_CORE) > TERMINAL_MEMORY_CORE_MAX_TOKENS) {
8237
+ throw new Error("Terminal always-on memory core exceeds 150 tokens");
8238
+ }
8239
+
8240
+ function memoryEmitterPromptFor(request, arch = loadArch(), turnId = null, permission = "write") {
8241
+ const stableId = String(turnId || "").replace(/[^A-Za-z0-9:._-]/g, "").slice(0, 160);
8242
+ let prompt = TERMINAL_MEMORY_CORE;
8243
+ if (stableId) prompt += `\nUse turn_id=${stableId}. permission=${permission === "read" ? "receipt-only" : "curated-write"}.`;
8244
+ if (!MEMORY_DETAIL_RE.test(String(request || ""))) return prompt;
8245
+ const kinds = Array.isArray(arch?.kinds) && arch.kinds.length ? arch.kinds.join("|") : "fact|decision|preference|risk|procedure";
8246
+ prompt += [
8247
+ "",
8248
+ `Allowed memory_kind: ${kinds}.`,
8249
+ "Global requires explicit owner authorization; suggest only, never promote.",
8250
+ "Do not emit request_context; put only a safe, short outcome in observation.",
8251
+ ].join("\n");
8252
+ return prompt;
8253
+ }
8254
+
8255
+ function credentialIndexReminderFor(request) {
8256
+ if (!CREDENTIAL_INDEX_RE.test(String(request || ""))) return "";
8257
+ return [
8258
+ "## Local credential lookup (triggered)",
8259
+ "Before saying a deploy, release, billing, auth, API, or cloud credential is missing, read `.agentlas/local-credentials.map.json` and the Local Credential Index in `.agentlas/project-soul-memory.md`.",
8260
+ "Use only env names and local relative references; never copy credential values into memory or output.",
8261
+ ].join("\n");
8262
+ }
8263
+
8264
+ function augmentSystem(db, baseSystem, ctx, withEmitter, request = "") {
7760
8265
  const arch = loadArch();
7761
8266
  let sys = baseSystem || "";
7762
8267
  // 언어/말투 지시를 맨 앞에 둔다. imported/cloud/company agents도 같은 전역 계약을 따른다.
@@ -7764,12 +8269,133 @@ function augmentSystem(db, baseSystem, ctx, withEmitter) {
7764
8269
  sys = langDirective(lang) + (sys ? "\n\n" + sys : "");
7765
8270
  const connectionSkill = loadGlobalConnectionSkill();
7766
8271
  if (connectionSkill) sys += "\n\n" + connectionSkill;
7767
- const mem = cliMemoryContext(db, ctx && ctx.projectPath);
8272
+ const mem = cliMemoryContext(db, ctx && ctx.projectPath, ctx && ctx.agentId);
7768
8273
  if (mem) sys += "\n\n" + mem;
7769
- if (withEmitter && arch.emitterBlock) sys += "\n\n" + arch.emitterBlock;
8274
+ if (withEmitter) {
8275
+ sys += "\n\n" + memoryEmitterPromptFor(request, arch, ctx && ctx.turnId, ctx && ctx.permission);
8276
+ const credentialReminder = credentialIndexReminderFor(request);
8277
+ if (credentialReminder) sys += "\n\n" + credentialReminder;
8278
+ }
7770
8279
  return sys;
7771
8280
  }
7772
8281
 
8282
+ function curatorRuntimeDirCli() {
8283
+ const root = path.join(userDataDir(), "memory-governance", "curator-runtime");
8284
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
8285
+ try { fs.chmodSync(path.dirname(root), 0o700); } catch { /* Windows/ACL-only host */ }
8286
+ try { fs.chmodSync(root, 0o700); } catch { /* Windows/ACL-only host */ }
8287
+ return root;
8288
+ }
8289
+
8290
+ function curatorRuntimeEnvCli(source = process.env) {
8291
+ // The semantic Curator has no tools and receives only pre-gated candidates.
8292
+ // Keep its process environment equally narrow: subscription CLIs can locate
8293
+ // their normal file-backed auth, but project/provider secret env is absent.
8294
+ const allowed = new Set([
8295
+ "PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "TMP", "TEMP",
8296
+ "LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "NO_COLOR",
8297
+ "CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", "USERPROFILE",
8298
+ "APPDATA", "LOCALAPPDATA", "SYSTEMROOT", "SystemRoot", "COMSPEC", "ComSpec", "PATHEXT",
8299
+ ]);
8300
+ const env = {};
8301
+ for (const [key, value] of Object.entries(source || {})) {
8302
+ if (allowed.has(key) || key.startsWith("LC_")) env[key] = value;
8303
+ }
8304
+ env.AGENTLAS_MEMORY_CURATOR = "1";
8305
+ return env;
8306
+ }
8307
+
8308
+ function ensureGeminiNoToolsPolicyCli() {
8309
+ const dir = curatorRuntimeDirCli();
8310
+ const file = path.join(dir, "gemini-no-tools-policy.toml");
8311
+ const content = [
8312
+ "# Managed by Agentlas Terminal for the semantic Memory Curator.",
8313
+ "[[rule]]",
8314
+ 'toolName = "*"',
8315
+ 'decision = "deny"',
8316
+ "priority = 999",
8317
+ "",
8318
+ ].join("\n");
8319
+ let current = null;
8320
+ try { current = fs.readFileSync(file, "utf8"); } catch { /* first write */ }
8321
+ if (current !== content) {
8322
+ const temp = path.join(dir, `.gemini-no-tools-policy.${process.pid}.${crypto.randomUUID()}.tmp`);
8323
+ fs.writeFileSync(temp, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
8324
+ fs.renameSync(temp, file);
8325
+ }
8326
+ try { fs.chmodSync(file, 0o600); } catch { /* Windows/ACL-only host */ }
8327
+ return file;
8328
+ }
8329
+
8330
+ async function invokeMemoryCuratorCli(db, runtime, model, payload, systemPrompt) {
8331
+ const serialized = JSON.stringify(payload);
8332
+ if (
8333
+ terminalMemoryGovernance.hasSecret(serialized) ||
8334
+ terminalMemoryGovernance.hasAbsolutePath(serialized) ||
8335
+ terminalMemoryGovernance.hasTranscriptBody(serialized)
8336
+ ) {
8337
+ throw new Error("Memory Curator payload failed the pre-invocation privacy gate");
8338
+ }
8339
+ if (runtime.mode === "cli") {
8340
+ return captureRuntime(runtime.kind, systemPrompt, serialized, {
8341
+ cwd: curatorRuntimeDirCli(),
8342
+ env: curatorRuntimeEnvCli(),
8343
+ permission: "read",
8344
+ model: model || runtime.model || null,
8345
+ effort: "low",
8346
+ authorityMode: "no-authority",
8347
+ noToolsPolicyPath: runtime.kind === "gemini" ? ensureGeminiNoToolsPolicyCli() : null,
8348
+ outputLimitBytes: 64 * 1024,
8349
+ timeoutConfig: { idleMs: 60_000, totalMs: 120_000, killGraceMs: 2_000 },
8350
+ });
8351
+ }
8352
+ return runApi(runtime.backend, model || runtime.model, systemPrompt, serialized);
8353
+ }
8354
+
8355
+ function beginMemoryTurnCli(db, prompt, ctx = {}) {
8356
+ return terminalMemoryGovernance.beginTurn(db, {
8357
+ prompt,
8358
+ projectPath: ctx.projectPath,
8359
+ agentId: ctx.agentId,
8360
+ permission: ctx.permission,
8361
+ surface: ctx.surface || "terminal-normal-turn",
8362
+ conversationRef: ctx.conversationRef,
8363
+ priorContextDigest: ctx.priorContextDigest,
8364
+ stableTurnId: ctx.stableTurnId || process.env.AGENTLAS_TURN_ID,
8365
+ });
8366
+ }
8367
+
8368
+ async function completeMemoryTurnCli(db, text, ctx, runtime, options = {}) {
8369
+ const turnId = ctx?.memoryTurn?.turnId || ctx?.turnId;
8370
+ const arch = loadArch();
8371
+ const runtimeInfo = runtime || {};
8372
+ const completion = {
8373
+ turnId,
8374
+ mainOutput: text,
8375
+ requestText: options.requestText,
8376
+ permission: ctx && ctx.permission,
8377
+ projectPath: ctx && ctx.projectPath,
8378
+ agentId: ctx && ctx.agentId,
8379
+ eventsHeading: arch.eventsHeading,
8380
+ outcome: options.outcome || "completed",
8381
+ coreFiles: {
8382
+ memoryDir: arch.memoryDir || ".agentlas",
8383
+ ticketFile: arch.memoryTicketsFile || "memory-tickets.jsonl",
8384
+ decisionFile: arch.curatorDecisionsFile || "curator-decisions.jsonl",
8385
+ },
8386
+ };
8387
+ if (options.invokeCurator !== false) {
8388
+ completion.invokeCurator = (payload, systemPrompt) => invokeMemoryCuratorCli(
8389
+ db,
8390
+ runtimeInfo,
8391
+ options.model || runtimeInfo.model || null,
8392
+ payload,
8393
+ systemPrompt,
8394
+ );
8395
+ }
8396
+ return terminalMemoryGovernance.completeTurn(db, completion);
8397
+ }
8398
+
7773
8399
  function loadGlobalConnectionSkill() {
7774
8400
  try {
7775
8401
  return require("../dist/electron/runtime/global-skill.js").GLOBAL_CONNECTION_SKILL || "";
@@ -7791,19 +8417,78 @@ const RUNTIME_BIN = {
7791
8417
 
7792
8418
  // 활성 런타임 → 실행 방식 결정. CLI(claude/codex/gemini) 또는 API(BYOK/Ollama).
7793
8419
  function resolveRuntime(db, override) {
8420
+ const ar = activeRuntime(db);
8421
+ const activeCli = ar && RUNTIME_BIN[ar.kind]
8422
+ ? {
8423
+ mode: "cli",
8424
+ kind: ar.kind,
8425
+ model: ar.model || null,
8426
+ capabilities: ["code", "tools", ...(ar.long_context ? ["long-context"] : [])],
8427
+ efforts: [],
8428
+ }
8429
+ : null;
7794
8430
  if (override) {
7795
- if (!RUNTIME_BIN[override]) fail(`알 없는 런타임: ${override} (claude-code|codex|gemini)`);
7796
- return { mode: "cli", kind: override };
8431
+ if (!RUNTIME_BIN[override]) fail(`Unknown runtime: ${override} (claude-code|codex|gemini)`);
8432
+ return activeCli && activeCli.kind === override ? activeCli : { mode: "cli", kind: override };
7797
8433
  }
7798
- const ar = activeRuntime(db);
7799
- if (ar && RUNTIME_BIN[ar.kind]) return { mode: "cli", kind: ar.kind };
8434
+ if (activeCli) return activeCli;
7800
8435
  if (ar && ar.kind === "byok" && ar.backend) return { mode: "api", backend: ar.backend, model: ar.model };
7801
8436
  if (ar && ar.kind === "ollama") return { mode: "api", backend: "ollama", model: ar.model };
7802
8437
  // 폴백: 설치된 CLI 탐지
7803
8438
  for (const kind of Object.keys(RUNTIME_BIN)) {
7804
8439
  if (which(RUNTIME_BIN[kind])) return { mode: "cli", kind };
7805
8440
  }
7806
- fail("사용할 런타임이 없습니다. CLI(claude/codex/gemini) 설치하거나 앱에서 API 키/Ollama 설정하세요.");
8441
+ fail("No runtime is available. Install a CLI (claude/codex/gemini) or configure an API key/Ollama in the app.");
8442
+ }
8443
+
8444
+ // Build the executable runtime inventory for the parent allocator. It is
8445
+ // intentionally local to this host: a Terminal/Codex/Claude plugin never
8446
+ // pretends it can schedule a runtime that is not installed and connected here.
8447
+ function listAvailableRuntimes(db, fallbackRuntime = null) {
8448
+ const routing = require("./agentlas-workload-routing.cjs");
8449
+ const active = fallbackRuntime || resolveRuntime(db);
8450
+ const candidates = [];
8451
+ const add = (runtime) => {
8452
+ if (!runtime) return;
8453
+ const key = runtime.mode === "cli" ? `cli:${runtime.kind}` : `api:${runtime.backend}:${runtime.model || ""}`;
8454
+ if (candidates.some((item) => item.key === key)) return;
8455
+ const discovered = routing.defaultAvailableModels(runtime);
8456
+ const availableModels = [...discovered];
8457
+ if (runtime.model && !availableModels.some((model) => model.id === runtime.model)) {
8458
+ availableModels.push({
8459
+ id: runtime.model,
8460
+ tier: runtime.modelTier || runtime.tier || null,
8461
+ capabilities: runtime.capabilities || [],
8462
+ contextWindow: runtime.contextWindow || null,
8463
+ efforts: runtime.efforts || [],
8464
+ description: runtime.modelDescription || "host-selected current model",
8465
+ });
8466
+ }
8467
+ candidates.push({ ...runtime, key, availableModels });
8468
+ };
8469
+ add(active);
8470
+ for (const kind of Object.keys(RUNTIME_BIN)) {
8471
+ if (!which(RUNTIME_BIN[kind])) continue;
8472
+ add({ mode: "cli", kind });
8473
+ }
8474
+ return candidates
8475
+ .filter((runtime) => runtime.availableModels.length)
8476
+ .map(({ key, ...runtime }, index) => ({ ...runtime, runtimeId: `runtime-${index + 1}` }));
8477
+ }
8478
+
8479
+ function currentRuntimeInventoryCli(db, runtime) {
8480
+ const candidates = listAvailableRuntimes(db, runtime);
8481
+ const current = candidates.find((candidate) =>
8482
+ candidate.mode === runtime.mode &&
8483
+ (runtime.mode === "cli"
8484
+ ? candidate.kind === runtime.kind
8485
+ : candidate.backend === runtime.backend && candidate.model === runtime.model));
8486
+ if (current) return current;
8487
+ return {
8488
+ ...runtime,
8489
+ runtimeId: "runtime-current",
8490
+ availableModels: workloadRouting.defaultAvailableModels(runtime),
8491
+ };
7807
8492
  }
7808
8493
 
7809
8494
  // ── API 러너 (BYOK / Ollama) — 비스트리밍, 최종 텍스트 반환 ──
@@ -7843,14 +8528,14 @@ function normalizeCustomApiBaseUrl(raw) {
7843
8528
  try {
7844
8529
  parsed = new URL(value);
7845
8530
  } catch {
7846
- throw new Error("Custom API base URL 올바르지 않습니다.");
8531
+ throw new Error("Custom API base URL is invalid.");
7847
8532
  }
7848
8533
  const host = parsed.hostname.toLowerCase();
7849
8534
  const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
7850
8535
  const isPrivateLan =
7851
8536
  /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
7852
8537
  if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && (isLoopback || isPrivateLan))) {
7853
- throw new Error("Custom API base URL HTTPS 또는 localhost/LAN의 HTTP여야 합니다.");
8538
+ throw new Error("Custom API base URL must use HTTPS or HTTP on localhost/LAN.");
7854
8539
  }
7855
8540
  return value.replace(/\/+$/, "");
7856
8541
  }
@@ -7876,7 +8561,7 @@ function readCustomApiBaseUrl() {
7876
8561
  raw = "";
7877
8562
  }
7878
8563
  } catch (e) {
7879
- throw new Error(`Custom API base URL 공유 DB에서 읽지 못했습니다: ${(e && e.message) || e}`);
8564
+ throw new Error(`Could not read the Custom API base URL from the shared database: ${(e && e.message) || e}`);
7880
8565
  } finally {
7881
8566
  try { if (db && typeof db.close === "function") db.close(); } catch { /* ignore close failure */ }
7882
8567
  }
@@ -7892,22 +8577,22 @@ async function runApi(backend, model, system, prompt, options) {
7892
8577
  options = options || {};
7893
8578
  model = model || DEFAULT_API_MODEL[backend];
7894
8579
  const fetchImpl = options.fetch || globalThis.fetch;
7895
- if (typeof fetchImpl !== "function") throw new Error(" 런타임에 fetch가 없습니다( 런타임으로 실행 필요).");
8580
+ if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime (run through the app runtime).");
7896
8581
  if (backend === "ollama") {
7897
8582
  const resp = await fetchImpl("http://127.0.0.1:11434/api/chat", {
7898
8583
  method: "POST",
7899
8584
  headers: { "content-type": "application/json" },
7900
8585
  body: JSON.stringify({ model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
7901
8586
  });
7902
- if (!resp.ok) throw new Error(`Ollama ${resp.status} — 'ollama serve' 실행/모델 확인`);
8587
+ if (!resp.ok) throw new Error(`Ollama ${resp.status} — run 'ollama serve' and check the model`);
7903
8588
  const j = await resp.json();
7904
8589
  return (j.message && j.message.content) || "";
7905
8590
  }
7906
8591
  const supported = backend === "anthropic" || backend === "openai" || backend === "google" ||
7907
8592
  backend === "upstage" || backend === "custom" || !!ANTHROPIC_COMPAT_API[backend];
7908
- if (!supported) throw new Error("지원하지 않는 backend: " + backend);
8593
+ if (!supported) throw new Error("Unsupported backend: " + backend);
7909
8594
  const key = Object.prototype.hasOwnProperty.call(options, "apiKey") ? options.apiKey : await apiKey(backend);
7910
- if (!key) throw new Error(`${backend} API 키가 없습니다. 설정 → BYOK에서 키를 등록하세요.`);
8595
+ if (!key) throw new Error(`${backend} API key is missing. Register it in App settings → BYOK.`);
7911
8596
 
7912
8597
  const anthropicCompat = ANTHROPIC_COMPAT_API[backend];
7913
8598
  if (backend === "anthropic" || anthropicCompat) {
@@ -7955,38 +8640,129 @@ async function runApi(backend, model, system, prompt, options) {
7955
8640
  const c = j.candidates && j.candidates[0];
7956
8641
  return (c && c.content && c.content.parts && c.content.parts[0] && c.content.parts[0].text) || "";
7957
8642
  }
7958
- throw new Error("지원하지 않는 backend: " + backend);
8643
+ throw new Error("Unsupported backend: " + backend);
7959
8644
  }
7960
8645
 
7961
8646
  // 1회 실행 — CLI면 spawn(스트리밍 stdout), API면 호출 후 텍스트 출력. 종료코드 반환.
7962
8647
  // ctx = { projectPath, agentId } — 메모리 주입/큐레이션에 사용.
8648
+ function finalizeExperienceExecutionCli(db, input) {
8649
+ if (input.permission === "read") return null;
8650
+ if (!input.agentId) return null;
8651
+ let agent;
8652
+ try { agent = db.prepare("SELECT * FROM installed_agents WHERE id=?").get(input.agentId); }
8653
+ catch { return null; }
8654
+ if (!agent) return null;
8655
+ const exactBase = exactAgentBaseForExecution(db, agent, input.runtimeExperience);
8656
+ if (!exactBase) return null;
8657
+ const runtime = input.runtime || {};
8658
+ const provider = runtime.mode === "cli" ? runtime.kind : runtime.backend;
8659
+ const modelId = input.model || runtime.model || provider;
8660
+ const usage = input.usage || {};
8661
+ try {
8662
+ return terminalExperienceIntake.finalizeAgentExecution({
8663
+ db,
8664
+ userDataDir: userDataDir(),
8665
+ cwd: input.cwd || input.projectPath || projectCwd(),
8666
+ agent,
8667
+ exactBase,
8668
+ environment: { runtime: provider || "terminal", os: process.platform, arch: process.arch },
8669
+ model: { provider: provider || "terminal-runtime", modelId: modelId || "terminal-runtime" },
8670
+ mcp: (input.mcpServers || []).flatMap((server) => {
8671
+ const catalogId = server.catalog_id || server.catalogId;
8672
+ // A reviewed runtime allowlist proves approval, not that this turn's
8673
+ // child completed an MCP initialize/tool call. Do not inflate it into
8674
+ // connected evidence without an exact runtime signal.
8675
+ return catalogId ? [{ catalogId, status: "approved" }] : [];
8676
+ }),
8677
+ outcome: input.outcome,
8678
+ metrics: {
8679
+ promptTokens: usage.input_tokens || usage.prompt_tokens || 0,
8680
+ completionTokens: usage.output_tokens || usage.completion_tokens || 0,
8681
+ totalTokens: usage.total_tokens || 0,
8682
+ durationMs: input.durationMs || usage.duration_ms || 0,
8683
+ retryCount: 0,
8684
+ },
8685
+ curatedMemories: input.curatedMemories || [],
8686
+ taskHint: input.taskHint,
8687
+ taskSignatures: input.runtimeExperience?.taskSignatures || [],
8688
+ experiencePackReleaseId: input.runtimeExperience?.experiencePackReleaseIds?.[0] || null,
8689
+ locale: input.lang || prefsLang(),
8690
+ runId: input.runId,
8691
+ createdAt: input.createdAt,
8692
+ });
8693
+ } catch (error) {
8694
+ process.stderr.write(`▸ local Experience intake skipped · ${String((error && error.message) || error).slice(0, 180)}\n`);
8695
+ return null;
8696
+ }
8697
+ }
8698
+
7963
8699
  async function executeOnce(db, system, prompt, override, ctx) {
7964
8700
  ctx = ctx || { projectPath: null, agentId: null };
8701
+ const runStartedAt = Date.now();
8702
+ const memoryTurn = beginMemoryTurnCli(db, prompt, {
8703
+ ...ctx,
8704
+ surface: ctx.surface || "terminal-one-shot",
8705
+ stableTurnId: ctx.turnId,
8706
+ });
8707
+ ctx.memoryTurn = memoryTurn;
8708
+ ctx.turnId = memoryTurn.turnId;
8709
+ const experienceRunId = `terminal-run:${memoryTurn.turnId}`;
8710
+ const curatedMemories = [];
8711
+ ctx.curatedMemories = curatedMemories;
7965
8712
  if (!ctx.cwdAtRequest) ctx.cwdAtRequest = projectCwd();
8713
+ let memoryRuntime = null;
8714
+ let memorySettled = false;
8715
+ try {
7966
8716
  let runtimeSystem = system;
7967
8717
  let localExperienceContext = null;
7968
- if (ctx.runtimeExperience && ctx.runtimeExperience.disabled !== true) {
8718
+ if (ctx.runtimeExperience?.disabled === true && ctx.runtimeExperience.observableReason) {
8719
+ process.stderr.write(`▸ local Experience skipped · ${ctx.runtimeExperience.observableReason}\n`);
8720
+ } else if (ctx.runtimeExperience && ctx.runtimeExperience.disabled !== true) {
7969
8721
  const runtimeExperience = ctx.runtimeExperience;
7970
8722
  const augmented = terminalExperienceExchange.augmentRuntimeSystemWithLocalExperience(system, {
7971
8723
  userDataDir: userDataDir(),
7972
8724
  cwd: ctx.projectPath || ctx.cwdAtRequest,
7973
8725
  baseAgentReleaseId: runtimeExperience.baseAgentReleaseId,
7974
8726
  agentDefinitionId: runtimeExperience.agentDefinitionId,
8727
+ experiencePackReleaseIds: runtimeExperience.experiencePackReleaseIds || [],
7975
8728
  taskSignatures: runtimeExperience.taskSignatures || [],
7976
8729
  environmentTags: Array.isArray(runtimeExperience.environmentTags) && runtimeExperience.environmentTags.length
7977
8730
  ? runtimeExperience.environmentTags
7978
8731
  : terminalExperienceExchange.defaultEnvironmentTags(),
8732
+ reservedTokens: ctx.runtimeExperience?.tasteRuntimeOverlay?.estimatedTokens ?? 0,
7979
8733
  });
7980
8734
  runtimeSystem = augmented.systemPrompt;
7981
8735
  localExperienceContext = augmented.experienceContext;
7982
8736
  if (localExperienceContext.itemIds.length) {
7983
- process.stderr.write(`▸ local Experience advisory · ${localExperienceContext.itemIds.length} item(s) · ~${localExperienceContext.estimatedTokens} tokens · no server rental receipt\n`);
8737
+ const source = runtimeExperience.loadoutAuthority === "desktop-terminal-exact-loadout"
8738
+ ? "Desktop-approved exact Experience"
8739
+ : "local Experience advisory";
8740
+ process.stderr.write(`▸ ${source} · ${localExperienceContext.itemIds.length} item(s) · ~${localExperienceContext.estimatedTokens} tokens · no server rental receipt\n`);
7984
8741
  }
7985
8742
  }
8743
+ const tasteTaskResolution = terminalExperienceExchange.deriveCanonicalTaskClasses(prompt);
8744
+ if (
8745
+ ctx.runtimeExperience?.tasteRuntimeOverlay &&
8746
+ desktopOntologyLoadout.tasteRuntimeOverlayMatchesTask(
8747
+ ctx.runtimeExperience.tasteRuntimeOverlay,
8748
+ tasteTaskResolution.taskIds,
8749
+ prompt,
8750
+ )
8751
+ ) {
8752
+ const tasteDirective = desktopOntologyLoadout.renderTasteRuntimeDirective(
8753
+ ctx.runtimeExperience.tasteRuntimeOverlay,
8754
+ );
8755
+ runtimeSystem = `${runtimeSystem}\n\n${tasteDirective}`;
8756
+ process.stderr.write(
8757
+ `▸ Desktop-approved exact Taste · ${ctx.runtimeExperience.tasteRuntimeOverlay.releaseId} · ~${ctx.runtimeExperience.tasteRuntimeOverlay.estimatedTokens} tokens · session snapshot\n`,
8758
+ );
8759
+ }
7986
8760
  const rt = resolveRuntime(db, override);
8761
+ memoryRuntime = rt;
7987
8762
  if (rt.mode === "cli") {
7988
- // 네이티브 CLI 자체 세션을 가지므로 emitter 넣지 않고(노이즈 방지) 메모리 컨텍스트만 주입.
7989
- const sys = augmentSystem(db, runtimeSystem, ctx, false);
8763
+ // 네이티브 CLI에도 같은 Memory emitter 주입하되 guard가 화면의 JSON 블록을 숨긴다.
8764
+ // 큐레이터가 만든 구조화 Memory만 성공 RunReceipt 이후 Experience intake로 전달된다.
8765
+ const sys = augmentSystem(db, runtimeSystem, ctx, true, prompt);
7990
8766
  const cwd = ctx.projectPath || projectCwd();
7991
8767
  const permission = ctx.permission || "write";
7992
8768
  const env = await buildChildEnvCli(db, { ...ctx, cwd });
@@ -7994,40 +8770,300 @@ async function executeOnce(db, system, prompt, override, ctx) {
7994
8770
  // one-shot(`agentlas "작업"`)도 REPL과 동일한 리치 렌더(⏺ 툴 / └ 결과 / 토큰)로 출력한다.
7995
8771
  const { runNativeTurn } = require("./agentlas-native-host.cjs");
7996
8772
  const { Ui } = require("./agentlas-ui.cjs");
8773
+ const { makeMemoryGuard } = require("./agentlas-repl.cjs");
7997
8774
  const ui = new Ui({ lang: prefsLang() });
7998
8775
  let mcpServers = [];
7999
8776
  if (permission === "full") {
8000
- try {
8001
- mcpServers = db.prepare("SELECT id, name, transport, command, args_json, enabled FROM mcp_servers WHERE enabled=1 AND transport='stdio'").all();
8002
- } catch { /* ignore */ }
8777
+ if (Array.isArray(ctx.mcpServers)) {
8778
+ // Build's reviewed host allowlist is authoritative, including the valid
8779
+ // empty list. Never fall back to every enabled registry row.
8780
+ mcpServers = ctx.mcpServers;
8781
+ } else {
8782
+ try {
8783
+ mcpServers = terminalAssets.readConsentedSystemMcpServers(db, { userDataDir: userDataDir() });
8784
+ } catch { /* ignore */ }
8785
+ }
8003
8786
  }
8004
8787
  ui.beginTurn();
8005
- const res = await runNativeTurn({
8006
- kind: rt.kind,
8007
- bin: which(RUNTIME_BIN[rt.kind]) || RUNTIME_BIN[rt.kind],
8008
- prompt,
8009
- systemPrompt: sys,
8788
+ const memoryGuard = makeMemoryGuard(ui, loadArch().eventsHeading);
8789
+ let res;
8790
+ try {
8791
+ res = await runNativeTurn({
8792
+ kind: rt.kind,
8793
+ bin: which(RUNTIME_BIN[rt.kind]) || RUNTIME_BIN[rt.kind],
8794
+ prompt,
8795
+ systemPrompt: sys,
8796
+ cwd,
8797
+ permission,
8798
+ session: {},
8799
+ model: ctx.model || null,
8800
+ effort: ctx.effort || null,
8801
+ mcpServers,
8802
+ mcpAllowlistMode: ctx.mcpAllowlistMode,
8803
+ env,
8804
+ ui: memoryGuard,
8805
+ });
8806
+ } finally {
8807
+ ui.endTurn();
8808
+ }
8809
+ const nativeText = String(res.text || "");
8810
+ const memoryResult = await completeMemoryTurnCli(db, nativeText, ctx, rt, {
8811
+ model: ctx.model || rt.model,
8812
+ outcome: res.error ? "failed" : "succeeded",
8813
+ requestText: prompt,
8814
+ // Every failed runtime needs a deterministic receipt, but must not
8815
+ // recursively call the runtime that just failed.
8816
+ invokeCurator: !res.error,
8817
+ });
8818
+ memorySettled = true;
8819
+ for (const memory of memoryResult.curatedMemories || []) {
8820
+ if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
8821
+ }
8822
+ finalizeExperienceExecutionCli(db, {
8823
+ agentId: ctx.agentId,
8824
+ projectPath: ctx.projectPath,
8010
8825
  cwd,
8011
- permission,
8012
- session: {},
8013
- model: null,
8014
- effort: null,
8826
+ runtime: rt,
8827
+ permission: ctx.permission,
8828
+ model: ctx.model || rt.model,
8829
+ runtimeExperience: ctx.runtimeExperience,
8015
8830
  mcpServers,
8016
- env,
8017
- ui,
8831
+ curatedMemories,
8832
+ taskHint: prompt,
8833
+ outcome: { status: res.error ? "failed" : "succeeded", failureCode: res.error ? "runtime-error" : null },
8834
+ usage: res.usage,
8835
+ durationMs: Date.now() - runStartedAt,
8836
+ runId: experienceRunId,
8837
+ lang: ctx.lang,
8018
8838
  });
8019
- ui.endTurn();
8020
8839
  return res.error ? 1 : 0;
8021
8840
  }
8022
8841
  // API 경로 — emitter 동봉 → 답변에서 메모리 이벤트를 파싱·큐레이션하고 블록은 제거.
8023
- const sys = augmentSystem(db, runtimeSystem, ctx, true);
8842
+ const sys = augmentSystem(db, runtimeSystem, ctx, true, prompt);
8024
8843
  const env = await buildChildEnvCli(db, { ...ctx, cwd: ctx.cwd || projectCwd() });
8025
8844
  Object.assign(process.env, env);
8026
- process.stderr.write(`▸ ${rt.backend}${rt.model ? " · " + rt.model : ""}\n`);
8027
- const text = await runApi(rt.backend, rt.model, sys, prompt);
8028
- const cleaned = curateCliReply(db, text || "", ctx);
8845
+ const selectedModel = ctx.model || rt.model;
8846
+ process.stderr.write(`▸ ${rt.backend}${selectedModel ? " · " + selectedModel : ""}\n`);
8847
+ let text;
8848
+ try {
8849
+ text = await runApi(rt.backend, selectedModel, sys, prompt);
8850
+ } catch (error) {
8851
+ finalizeExperienceExecutionCli(db, {
8852
+ agentId: ctx.agentId,
8853
+ projectPath: ctx.projectPath,
8854
+ cwd: ctx.cwd || projectCwd(),
8855
+ runtime: rt,
8856
+ permission: ctx.permission,
8857
+ model: selectedModel,
8858
+ runtimeExperience: ctx.runtimeExperience,
8859
+ curatedMemories,
8860
+ taskHint: prompt,
8861
+ outcome: { status: "failed", failureCode: "runtime-error" },
8862
+ durationMs: Date.now() - runStartedAt,
8863
+ runId: experienceRunId,
8864
+ lang: ctx.lang,
8865
+ });
8866
+ throw error;
8867
+ }
8868
+ const memoryResult = await completeMemoryTurnCli(db, text || "", ctx, rt, {
8869
+ model: selectedModel,
8870
+ outcome: "succeeded",
8871
+ requestText: prompt,
8872
+ });
8873
+ memorySettled = true;
8874
+ for (const memory of memoryResult.curatedMemories || []) {
8875
+ if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
8876
+ }
8877
+ const cleaned = require("./agentlas-style.cjs").sanitizeAssistantText(memoryResult.cleaned || "");
8878
+ finalizeExperienceExecutionCli(db, {
8879
+ agentId: ctx.agentId,
8880
+ projectPath: ctx.projectPath,
8881
+ cwd: ctx.cwd || projectCwd(),
8882
+ runtime: rt,
8883
+ permission: ctx.permission,
8884
+ model: selectedModel,
8885
+ runtimeExperience: ctx.runtimeExperience,
8886
+ curatedMemories,
8887
+ taskHint: prompt,
8888
+ outcome: { status: "succeeded", failureCode: null },
8889
+ durationMs: Date.now() - runStartedAt,
8890
+ runId: experienceRunId,
8891
+ lang: ctx.lang,
8892
+ });
8029
8893
  process.stdout.write((cleaned || "").trim() + "\n");
8030
8894
  return 0;
8895
+ } catch (error) {
8896
+ if (!memorySettled) {
8897
+ try {
8898
+ await completeMemoryTurnCli(db, "", ctx, memoryRuntime, {
8899
+ model: ctx.model || memoryRuntime?.model || null,
8900
+ outcome: "failed",
8901
+ requestText: prompt,
8902
+ invokeCurator: false,
8903
+ });
8904
+ memorySettled = true;
8905
+ } catch {
8906
+ // A missing/locked DB is the only remaining case where a receipt may
8907
+ // be impossible. Preserve the original runtime error for the caller.
8908
+ }
8909
+ }
8910
+ throw error;
8911
+ }
8912
+ }
8913
+
8914
+ async function runTerminalBuilder(db, request, metadata = {}, runtimeOverride = null, cwd = projectCwd()) {
8915
+ const builder = resolveMetaBuilder(db);
8916
+ if (!builder) throw new Error("Agentlas Core Engine Meta-Agent is unavailable; Build did not start.");
8917
+ const runtime = resolveRuntime(db, runtimeOverride);
8918
+ const routingOptions = metadata.workloadRouting && typeof metadata.workloadRouting === "object"
8919
+ ? metadata.workloadRouting
8920
+ : {};
8921
+ let allocation = null;
8922
+ try {
8923
+ const currentInventory = currentRuntimeInventoryCli(db, runtime);
8924
+ const plannerSystem = workloadRouting.plannerSystemPrompt({
8925
+ language: prefsLang() === "ko" ? "Korean" : "English",
8926
+ maxTasks: 1,
8927
+ mode: "builder",
8928
+ liveRuntimeInventory: workloadRouting.runtimeInventory([currentInventory]),
8929
+ });
8930
+ let plannerText;
8931
+ if (runtime.mode === "cli") {
8932
+ const env = await buildChildEnvCli(db, { projectPath: cwd, agentId: builder.id, permission: "read", cwd });
8933
+ plannerText = await captureRuntime(runtime.kind, plannerSystem, request, {
8934
+ cwd,
8935
+ env,
8936
+ permission: "read",
8937
+ model: routingOptions.modelPin || runtime.model || null,
8938
+ effort: routingOptions.effortPin === undefined ? null : routingOptions.effortPin,
8939
+ });
8940
+ } else {
8941
+ plannerText = await runApi(runtime.backend, routingOptions.modelPin || runtime.model, plannerSystem, request);
8942
+ }
8943
+ const plan = workloadRouting.normalizePlan(plannerText, { maxTasks: 1 });
8944
+ allocation = plan && plan.tasks[0] && plan.tasks[0].allocation;
8945
+ } catch (error) {
8946
+ process.stderr.write(`▸ builder model planner fallback · ${String((error && error.message) || error).slice(0, 160)}\n`);
8947
+ }
8948
+ const currentInventory = currentRuntimeInventoryCli(db, runtime);
8949
+ const resolution = workloadRouting.resolveAllocation({
8950
+ runtime: currentInventory,
8951
+ decision: allocation,
8952
+ modelPin: routingOptions.modelPin,
8953
+ effortPin: routingOptions.effortPin,
8954
+ availableModels: currentInventory.availableModels,
8955
+ maxTier: routingOptions.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
8956
+ });
8957
+ const receipt = workloadRouting.createDecisionReceipt({
8958
+ taskId: "builder-execution",
8959
+ stage: "builder",
8960
+ decision: allocation,
8961
+ resolution,
8962
+ });
8963
+ try {
8964
+ workloadRouting.appendDecisionReceipt(receipt, path.join(userDataDir(), "model-routing-receipts.jsonl"));
8965
+ } catch (error) {
8966
+ process.stderr.write(`▸ builder model routing receipt failed · ${String((error && error.message) || error).slice(0, 120)}\n`);
8967
+ }
8968
+ if (!resolution.ok) {
8969
+ throw new Error(`Agentlas builder model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
8970
+ }
8971
+ process.stderr.write(
8972
+ `▸ builder model route · ${resolution.source} · ${resolution.model || runtime.kind || runtime.backend}` +
8973
+ `${resolution.effort ? ` · ${resolution.effort}` : ""}` +
8974
+ `${resolution.fallbackReason ? ` · ${resolution.fallbackReason}` : ""}\n`,
8975
+ );
8976
+ const code = await executeOnce(db, agentSystemPromptCli(builder), request, runtimeOverride, {
8977
+ projectPath: cwd,
8978
+ agentId: builder.id,
8979
+ permission: "full",
8980
+ // This is an exact private host object created after consent. An empty array
8981
+ // deliberately overrides all global/project/default MCP configuration.
8982
+ mcpServers: Array.isArray(metadata.mcpServers) ? metadata.mcpServers : [],
8983
+ mcpAllowlistMode: "exact",
8984
+ model: resolution.model,
8985
+ effort: resolution.effort,
8986
+ });
8987
+ if (code !== 0) throw new Error(`Agentlas builder runtime exited ${code}`);
8988
+ return code;
8989
+ }
8990
+
8991
+ async function allocateSingleWorkloadCli(db, request, options = {}) {
8992
+ const runtime = options.runtime || resolveRuntime(db, options.runtimeOverride);
8993
+ const cwd = options.cwd || projectCwd();
8994
+ let allocation = null;
8995
+ try {
8996
+ const currentInventory = currentRuntimeInventoryCli(db, runtime);
8997
+ const plannerSystem = workloadRouting.plannerSystemPrompt({
8998
+ language: options.lang === "ko" ? "Korean" : "English",
8999
+ maxTasks: 1,
9000
+ mode: options.mode || "team",
9001
+ liveRuntimeInventory: workloadRouting.runtimeInventory([currentInventory]),
9002
+ });
9003
+ let plannerText;
9004
+ if (runtime.mode === "cli") {
9005
+ const env = await buildChildEnvCli(db, {
9006
+ projectPath: options.projectPath || null,
9007
+ agentId: options.agentId || null,
9008
+ permission: "read",
9009
+ cwd,
9010
+ });
9011
+ plannerText = await captureRuntime(runtime.kind, plannerSystem, request, {
9012
+ cwd,
9013
+ env,
9014
+ permission: "read",
9015
+ model: options.modelPin || runtime.model || null,
9016
+ effort: options.effortPin === undefined ? null : options.effortPin,
9017
+ });
9018
+ } else {
9019
+ plannerText = await runApi(runtime.backend, options.modelPin || runtime.model, plannerSystem, request);
9020
+ }
9021
+ const plan = workloadRouting.normalizePlan(plannerText, { maxTasks: 1 });
9022
+ allocation = plan && plan.tasks[0] && plan.tasks[0].allocation;
9023
+ } catch (error) {
9024
+ if (options.onWarning) options.onWarning(`model planner fallback: ${String((error && error.message) || error).slice(0, 160)}`);
9025
+ }
9026
+ const currentInventory = currentRuntimeInventoryCli(db, runtime);
9027
+ const resolution = workloadRouting.resolveAllocation({
9028
+ runtime: currentInventory,
9029
+ decision: allocation,
9030
+ modelPin: options.modelPin,
9031
+ effortPin: options.effortPin,
9032
+ availableModels: options.availableModels || currentInventory.availableModels,
9033
+ maxTier: options.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
9034
+ });
9035
+ const receipt = workloadRouting.createDecisionReceipt({
9036
+ taskId: options.taskId || `${options.mode || "team"}-execution`,
9037
+ stage: options.mode || "team",
9038
+ decision: allocation,
9039
+ resolution,
9040
+ });
9041
+ try {
9042
+ workloadRouting.appendDecisionReceipt(receipt, options.receiptFile || path.join(userDataDir(), "model-routing-receipts.jsonl"));
9043
+ } catch (error) {
9044
+ if (options.onWarning) options.onWarning(`model routing receipt failed: ${String((error && error.message) || error).slice(0, 120)}`);
9045
+ }
9046
+ if (!resolution.ok) {
9047
+ throw new Error(`Agentlas model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
9048
+ }
9049
+ return { allocation, resolution, receipt };
9050
+ }
9051
+
9052
+ async function probeApprovedTerminalMcp(db, server, runtimeOverride, cwd, probeOptions = {}) {
9053
+ const runtime = resolveRuntime(db, runtimeOverride);
9054
+ if (runtime.mode !== "cli") return { connected: false, reason: "runtime_incompatible" };
9055
+ const env = await buildChildEnvCli(db, { cwd, permission: "full" });
9056
+ if (runtime.kind === "gemini") {
9057
+ const readiness = require("./agentlas-native-host.cjs").geminiMcpIsolationReadiness(env);
9058
+ if (!readiness.ready) return { connected: false, reason: "runtime_isolation_unavailable" };
9059
+ }
9060
+ return terminalAssets.probeSystemMcpServerConnection(server, {
9061
+ cwd,
9062
+ env,
9063
+ userDataDir: userDataDir(),
9064
+ timeoutMs: probeOptions.timeoutMs,
9065
+ signal: probeOptions.signal,
9066
+ });
8031
9067
  }
8032
9068
 
8033
9069
  // API 백엔드용 간이 대화형 REPL (네이티브 인터랙티브가 없는 BYOK/Ollama).
@@ -8036,7 +9072,7 @@ function apiRepl(db, backend, model, system, label, ctx) {
8036
9072
  ctx = ctx || { projectPath: null, agentId: null };
8037
9073
  if (!ctx.cwdAtRequest) ctx.cwdAtRequest = ctx.cwd || projectCwd();
8038
9074
  const readline = require("node:readline");
8039
- process.stderr.write(`▸ ${label} (${backend}${model ? " · " + model : ""}) — 종료: /exit\n`);
9075
+ process.stderr.write(`▸ ${label} (${backend}${model ? " · " + model : ""}) — type /exit to stop\n`);
8040
9076
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
8041
9077
  const ask = () =>
8042
9078
  rl.question("\nyou › ", async (line) => {
@@ -8044,7 +9080,7 @@ function apiRepl(db, backend, model, system, label, ctx) {
8044
9080
  if (tt === "/exit" || tt === "/quit") return rl.close();
8045
9081
  if (!tt) return ask();
8046
9082
  try {
8047
- const sys = augmentSystem(db, system, ctx, true);
9083
+ const sys = augmentSystem(db, system, ctx, true, tt);
8048
9084
  const text = await runApi(backend, model, sys, tt);
8049
9085
  const cleaned = curateCliReply(db, text || "", ctx);
8050
9086
  process.stdout.write("\n" + (cleaned || "").trim() + "\n");
@@ -8090,12 +9126,6 @@ function runCwd() {
8090
9126
  }
8091
9127
  }
8092
9128
 
8093
- function cliMcpConfigPath() {
8094
- return require("./agentlas-native-host.cjs").cliMcpConfigPath([]).file;
8095
- }
8096
-
8097
- const CODEX_PLAYWRIGHT_MCP_ARGS = require("./agentlas-native-host.cjs").codexMcpArgs([]);
8098
-
8099
9129
  // 에이전트가 실제로 실행될 작업 폴더 = 사용자가 명령을 친 현재 디렉터리(= 대상 프로젝트).
8100
9130
  // 단, home/userData/agent-cwd 같은 "프로젝트 아님" 위치면 안전한 전용 폴더로 폴백한다.
8101
9131
  function projectCwd() {
@@ -8251,29 +9281,89 @@ async function buildChildEnvCli(db, ctx) {
8251
9281
 
8252
9282
  // One-shot/background capture uses the same permission truth as the interactive host.
8253
9283
  // Keep its plain-output argument shape, but never duplicate the security mapping here.
8254
- function buildArgs(kind, systemPrompt, prompt, permission) {
9284
+ function buildArgs(kind, systemPrompt, prompt, permission, runtimeOptions = {}) {
8255
9285
  const native = require("./agentlas-native-host.cjs");
8256
9286
  const level = require("./agentlas-permissions.cjs").normalize(permission);
9287
+ const model = runtimeOptions.model ? String(runtimeOptions.model) : null;
9288
+ const effort = runtimeOptions.effort ? String(runtimeOptions.effort) : null;
9289
+ const noAuthority = runtimeOptions.authorityMode === "no-authority";
8257
9290
  if (kind === "claude-code") {
8258
- const perm = native.claudePermissionArgs(level);
8259
- const mcp = level === "full"
8260
- ? ["--strict-mcp-config", "--mcp-config", cliMcpConfigPath(), "--allowedTools", "mcp__playwright"]
8261
- : native.claudeMcpIsolationArgs();
8262
- return ["-p", prompt, "--append-system-prompt", systemPrompt, ...perm, ...mcp];
9291
+ const perm = native.claudePermissionArgs(noAuthority ? "read" : level);
9292
+ // Background/capture has no one-pass reviewed server list. Full permission
9293
+ // changes tool authority, not MCP consent, so this path remains exact-empty.
9294
+ const mcp = native.claudeMcpIsolationArgs();
9295
+ const thinking = effort === "max" || effort === "xhigh" ? "Ultrathink. " : effort === "high" ? "Think hard. " : effort === "medium" ? "Think. " : "";
9296
+ const claudeEffort = effort === "minimal" ? "low" : effort === "xhigh" ? "max" : effort;
9297
+ const effortArgs = claudeEffort && claudeEffort !== "none" ? ["--effort", claudeEffort] : [];
9298
+ return [
9299
+ "-p", thinking + prompt,
9300
+ "--append-system-prompt", systemPrompt,
9301
+ ...(model ? ["--model", model] : []),
9302
+ ...effortArgs,
9303
+ ...perm,
9304
+ ...(noAuthority ? ["--tools", ""] : []),
9305
+ ...mcp,
9306
+ ];
8263
9307
  }
8264
9308
  if (kind === "codex") {
8265
- const perm = native.codexPermissionArgs(level);
8266
- const mcp = level === "full" ? CODEX_PLAYWRIGHT_MCP_ARGS : [];
8267
- return ["exec", "--skip-git-repo-check", ...perm, ...mcp, `[SYSTEM]\n${systemPrompt}\n\n${prompt}`];
9309
+ const perm = native.codexPermissionArgs(noAuthority ? "read" : level);
9310
+ const mcp = [];
9311
+ const modelArgs = model ? ["-m", model] : [];
9312
+ const effortArgs = effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
9313
+ const noAuthorityArgs = noAuthority ? [
9314
+ "--ephemeral",
9315
+ "--ignore-user-config",
9316
+ "--ignore-rules",
9317
+ "--disable", "shell_tool",
9318
+ "--disable", "unified_exec",
9319
+ "--disable", "apps",
9320
+ "--disable", "browser_use",
9321
+ "--disable", "computer_use",
9322
+ "--disable", "image_generation",
9323
+ "--disable", "workspace_dependencies",
9324
+ "--disable", "goals",
9325
+ "--disable", "memories",
9326
+ "--disable", "plugins",
9327
+ "--disable", "hooks",
9328
+ "--disable", "multi_agent",
9329
+ "--disable", "tool_suggest",
9330
+ "--json",
9331
+ ] : [];
9332
+ return ["exec", "--skip-git-repo-check", ...noAuthorityArgs, ...modelArgs, ...effortArgs, ...perm, ...mcp, `[SYSTEM]\n${systemPrompt}\n\n${prompt}`];
8268
9333
  }
8269
9334
  if (kind === "gemini") {
8270
- const perm = native.geminiPermissionArgs(level);
8271
- const mcp = level === "full" ? [] : native.geminiMcpIsolationArgs();
8272
- return ["--prompt", `[SYSTEM]\n${systemPrompt}\n\n${prompt}`, ...perm, ...mcp];
9335
+ const perm = native.geminiPermissionArgs(noAuthority ? "read" : level);
9336
+ if (noAuthority && !runtimeOptions.noToolsPolicyPath) {
9337
+ throw new Error("Gemini no-authority capture requires an explicit deny-all policy");
9338
+ }
9339
+ const noAuthorityArgs = noAuthority
9340
+ ? ["--admin-policy", String(runtimeOptions.noToolsPolicyPath)]
9341
+ : [];
9342
+ // Legacy/background capture has no structured reviewed server list. Even
9343
+ // at full permission it must stay exact-empty instead of inheriting the
9344
+ // user's global Gemini MCP definitions with the provider credential env.
9345
+ const mcp = native.geminiMcpIsolationArgs();
9346
+ return ["--prompt", `[SYSTEM]\n${systemPrompt}\n\n${prompt}`, ...(model ? ["-m", model] : []), ...perm, ...noAuthorityArgs, ...mcp];
8273
9347
  }
8274
9348
  return [prompt];
8275
9349
  }
8276
9350
 
9351
+ function codexCaptureAgentText(jsonl) {
9352
+ const completed = [];
9353
+ const latest = new Map();
9354
+ for (const line of String(jsonl || "").split(/\r?\n/)) {
9355
+ if (!line.trim()) continue;
9356
+ let event;
9357
+ try { event = JSON.parse(line); } catch { continue; }
9358
+ const item = event?.item;
9359
+ if (!item || item.type !== "agent_message" || typeof item.text !== "string") continue;
9360
+ if (event.type === "item.completed") completed.push(item.text);
9361
+ else if (event.type === "item.started" || event.type === "item.updated") latest.set(String(item.id || latest.size), item.text);
9362
+ }
9363
+ if (completed.length) return completed.join("");
9364
+ return [...latest.values()].join("");
9365
+ }
9366
+
8277
9367
  // `claude` 치면 바로 대화형 세션 뜨듯이 — 에이전트 폴더(CLAUDE.md/AGENTS.md/GEMINI.md 보유)에서
8278
9368
  // 네이티브 CLI를 인자 없이(대화형) 실행. 에이전트 페르소나는 그 폴더의 프로젝트 지시로 자동 로드. (A+B 결합)
8279
9369
  // 보스턴테리어 터미널(대화형 TUI)로 진입. agentlas 가 항상 "호스트"다 —
@@ -8296,14 +9386,23 @@ function buildHelpers(db) {
8296
9386
  return {
8297
9387
  which,
8298
9388
  RUNTIME_BIN,
8299
- augmentSystem: (db_, base, ctx, emit) => augmentSystem(db_, base, ctx, emit),
8300
- curateCliReply: (db_, text, ctx) => curateCliReply(db_, text, ctx),
9389
+ augmentSystem: (db_, base, ctx, emit, request) => augmentSystem(db_, base, ctx, emit, request),
9390
+ memoryEmitterPrompt: (request, ctx) => memoryEmitterPromptFor(
9391
+ request,
9392
+ loadArch(),
9393
+ ctx && ctx.turnId,
9394
+ ctx && ctx.permission,
9395
+ ),
9396
+ beginMemoryTurn: (db_, prompt, ctx) => beginMemoryTurnCli(db_, prompt, ctx),
9397
+ completeMemoryTurn: (db_, text, ctx, runtime, options) => completeMemoryTurnCli(db_, text, ctx, runtime, options),
8301
9398
  detectResponseLanguage: (prompt, fallback) => require("./agentlas-style.cjs").detectResponseLanguage(prompt, fallback),
8302
9399
  sanitizeAssistantText: (text) => require("./agentlas-style.cjs").sanitizeAssistantText(text),
8303
9400
  apiKey: (backend) => apiKey(backend),
8304
9401
  eventsHeading: () => loadArch().eventsHeading,
8305
9402
  defaultApiModel: (backend) => DEFAULT_API_MODEL[backend],
8306
9403
  buildChildEnv: (db_, ctx) => buildChildEnvCli(db_, ctx),
9404
+ allocateWorkload: (db_, request, ctx) => allocateSingleWorkloadCli(db_, request, ctx),
9405
+ finalizeExperienceRun: (db_, input) => finalizeExperienceExecutionCli(db_, input),
8307
9406
  multimodalStatus: (db_) => multimodalStatusCli(db_),
8308
9407
  setMultimodal: (db_, modality, providerId) => setMultimodalCli(db_, modality, providerId),
8309
9408
  resolveAgent,
@@ -8315,11 +9414,11 @@ function buildHelpers(db) {
8315
9414
  autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
8316
9415
  autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
8317
9416
  directSystemPrompt: (lang) => directSystemPrompt(lang),
8318
- cliMemoryContext: (db_, pp) => cliMemoryContext(db_, pp),
9417
+ cliMemoryContext: (db_, pp, agentId) => cliMemoryContext(db_, pp, agentId),
8319
9418
  importLocal: (db_, p) => importLocalFolderCli(db_, p),
8320
9419
  // REPL-safe public Hub install: fail()(process.exit) 대신 Error를 throw 해 REPL이 직접 렌더하게 한다.
8321
9420
  cloudInstall: async (db_, slug) => {
8322
- if (typeof fetch !== "function") throw new Error(" 런타임에 fetch가 없습니다( 런타임 필요).");
9421
+ if (typeof fetch !== "function") throw new Error("fetch is unavailable in this runtime (app runtime required).");
8323
9422
  const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
8324
9423
  const headers = { "content-type": "application/json" };
8325
9424
  const cookie = await cloudSessionCookieCli();
@@ -8332,18 +9431,18 @@ function buildHelpers(db) {
8332
9431
  body: JSON.stringify({ method: "marketplace.get_manifest", params: { name: "marketplace.get_manifest", arguments: { kind: "agent", slug } } }),
8333
9432
  });
8334
9433
  } catch (e) {
8335
- throw new Error(`Hub 연결 실패: ${(e && e.message) || e}`);
9434
+ throw new Error(`Hub connection failed: ${(e && e.message) || e}`);
8336
9435
  }
8337
9436
  if (!resp.ok) {
8338
9437
  const authHint = resp.status === 401 || resp.status === 403 ? " — 로그인이 필요합니다 (앱에서 로그인 또는 AGENTLAS_SESSION 설정)" : "";
8339
- throw new Error(`Hub 응답 ${resp.status}${authHint}`);
9438
+ throw new Error(`Hub returned HTTP ${resp.status}${authHint}`);
8340
9439
  }
8341
9440
  const json = parseHubJsonCli(resp, "marketplace.get_manifest");
8342
9441
  if (json.error) throw new Error(json.error.message || "Hub error");
8343
9442
  const listing = json.result;
8344
- if (!listing) throw new Error(`Hub에서 찾을 없음: ${slug}`);
9443
+ if (!listing) throw new Error(`Not found in Hub: ${slug}`);
8345
9444
  if (listing.delivery && listing.delivery.mode === "call_only") {
8346
- throw new Error(`이 Hub 에이전트는 call-only 자산입니다. 실행: agentlas call ${slug}`);
9445
+ throw new Error(`This Hub agent is call-only. Run: agentlas call ${slug}`);
8347
9446
  }
8348
9447
  return persistCloudListingCli(db_, listing);
8349
9448
  },
@@ -8352,7 +9451,22 @@ function buildHelpers(db) {
8352
9451
  },
8353
9452
  mcpServers: (db_) => {
8354
9453
  try {
8355
- return db_.prepare("SELECT id, name, name_en, transport, command, args_json, url, env_keys_json, enabled FROM mcp_servers ORDER BY installed_at ASC").all();
9454
+ const consentedIds = new Set(
9455
+ terminalAssets.readConsentedSystemMcpServers(db_, { userDataDir: userDataDir(), createRuntimeHome: false })
9456
+ .map((server) => server.id),
9457
+ );
9458
+ return db_.prepare("SELECT id, catalog_id, name, name_en, transport, command, args_json, url, env_keys_json, enabled FROM mcp_servers ORDER BY installed_at ASC")
9459
+ .all()
9460
+ .map((row) => {
9461
+ const runtime = terminalAssets.materializeTrustedSystemMcpServer(row, { userDataDir: userDataDir(), createRuntimeHome: false });
9462
+ Object.defineProperty(row, "runtimeEligible", { value: Boolean(runtime), enumerable: false });
9463
+ Object.defineProperty(row, "runtimeConsented", { value: Boolean(runtime && consentedIds.has(String(row.id))), enumerable: false });
9464
+ if (runtime) {
9465
+ Object.defineProperty(row, "credentialKeyNames", { value: runtime.credentialKeyNames, enumerable: false });
9466
+ if (runtime.mcpRuntimeHome) Object.defineProperty(row, "mcpRuntimeHome", { value: runtime.mcpRuntimeHome, enumerable: false });
9467
+ }
9468
+ return row;
9469
+ });
8356
9470
  } catch {
8357
9471
  return [];
8358
9472
  }
@@ -8367,6 +9481,7 @@ function buildHelpers(db) {
8367
9481
  // 패리티: REPL의 /storm·/swarm·/build·/route·/research 가 그대로 호출한다.
8368
9482
  stormRun: (db_, goal, ctx) => parity().stormRun(db_, goal, ctx),
8369
9483
  swarmRun: (db_, goal, ctx) => parity().swarmRun(db_, goal, ctx),
9484
+ workforceRun: (db_, goal, ctx) => workforce().workforceRun(db_, goal, ctx),
8370
9485
  terminalBuild: (db_, args, ctx = {}) => terminalAssets.cmdBuild({
8371
9486
  db: db_,
8372
9487
  args: Array.isArray(args) ? args : terminalAssets.tokenizeBuildCommandLine(String(args || "")),
@@ -8375,7 +9490,15 @@ function buildHelpers(db) {
8375
9490
  input: ctx.input || process.stdin,
8376
9491
  promptOutput: ctx.promptOutput || process.stderr,
8377
9492
  out: ctx.out || out,
8378
- invokeBuild: (request) => parity().cmdHep(db_, request ? ["hep-build", request] : ["hep-build"]),
9493
+ probeMcpServer: (server, probeOptions) => probeApprovedTerminalMcp(db_, server, null, ctx.cwd || projectCwd(), probeOptions),
9494
+ invokeBuild: (request, metadata) => runTerminalBuilder(db_, request, {
9495
+ ...metadata,
9496
+ workloadRouting: {
9497
+ modelPin: ctx.modelPin || null,
9498
+ effortPin: ctx.effortPin,
9499
+ maxTier: ctx.maxTier,
9500
+ },
9501
+ }, null, ctx.cwd || projectCwd()),
8379
9502
  }),
8380
9503
  hepRun: (args, opts) => parity().runHephaestusInteractive(args, opts),
8381
9504
  cloudSearch: (db_, args) => parity().cloudSearch(db_, args),
@@ -8397,12 +9520,14 @@ function buildHelpers(db) {
8397
9520
  return null;
8398
9521
  }
8399
9522
  },
9523
+ ensureProjectForExecution: (db_, dir, permission, reason) =>
9524
+ ensureTerminalProjectForExecutionCli(db_, dir, permission, reason || "terminal-interactive-turn"),
8400
9525
  doctor: async (db_, ui) => {
8401
9526
  ui.line("");
8402
9527
  ui.info("userData: " + userDataDir());
8403
- ui.info("db: " + (fs.existsSync(dbPath()) ? "OK" : "없음"));
9528
+ ui.info("db: " + (fs.existsSync(dbPath()) ? "OK" : "missing"));
8404
9529
  const ar = activeRuntime(db_);
8405
- ui.info("활성 런타임: " + (ar ? ar.kind : "(없음)"));
9530
+ ui.info("Active runtime: " + (ar ? ar.kind : "(none)"));
8406
9531
  // CLI 런타임: 설치 + 로그인(인증 파일) 휴리스틱
8407
9532
  const home = os.homedir();
8408
9533
  const authFiles = {
@@ -8414,17 +9539,17 @@ function buildHelpers(db) {
8414
9539
  for (const [kind, bin] of Object.entries(RUNTIME_BIN)) {
8415
9540
  const installed = !!which(bin);
8416
9541
  const authed = (authFiles[kind] || []).some(has);
8417
- ui.info(` ${kind.padEnd(12)} ${!installed ? "미설치" : authed ? "설치됨 · 로그인" : "설치됨 · 로그인 미확인"}`);
9542
+ ui.info(` ${kind.padEnd(12)} ${!installed ? "not installed" : authed ? "installed · signed in" : "installed · sign-in unverified"}`);
8418
9543
  }
8419
9544
  // BYOK 키 (keytar) + 클라우드 세션
8420
9545
  const byok = [];
8421
9546
  for (const b of ["anthropic", "openai", "google", "upstage"]) {
8422
9547
  try { if (await apiKey(b)) byok.push(b); } catch { /* keytar 미사용 */ }
8423
9548
  }
8424
- ui.info("BYOK 키: " + (byok.length ? byok.join(", ") : "(없음 설정 → BYOK)"));
9549
+ ui.info("BYOK keys: " + (byok.length ? byok.join(", ") : "(noneApp settings → BYOK)"));
8425
9550
  let cloud = false;
8426
9551
  try { cloud = !!(await cloudSessionCookieCli()); } catch { /* ignore */ }
8427
- ui.info("클라우드 세션: " + (cloud ? "로그인됨" : "로그아웃"));
9552
+ ui.info("Cloud session: " + (cloud ? "signed in" : "signed out"));
8428
9553
  },
8429
9554
  };
8430
9555
  }
@@ -8465,14 +9590,18 @@ function spawnRuntime(kind, systemPrompt, prompt, opts) {
8465
9590
  const cwd = opts.cwd || runCwd();
8466
9591
  return new Promise((resolve) => {
8467
9592
  const bin = which(RUNTIME_BIN[kind]) || RUNTIME_BIN[kind];
8468
- const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env);
9593
+ const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env, {
9594
+ permission: opts.permission,
9595
+ mcpServers: [],
9596
+ mcpAllowlistMode: kind === "gemini" ? "exact" : undefined,
9597
+ });
8469
9598
  const child = spawn(bin, buildArgs(kind, systemPrompt, prompt, opts.permission), {
8470
9599
  cwd,
8471
9600
  stdio: ["ignore", "inherit", "inherit"],
8472
9601
  env,
8473
9602
  });
8474
9603
  child.on("error", (err) => {
8475
- process.stderr.write(`\n실행 실패(${kind}): ${err.message}\n`);
9604
+ process.stderr.write(`\nExecution failed (${kind}): ${err.message}\n`);
8476
9605
  resolve(1);
8477
9606
  });
8478
9607
  child.on("close", (code) => resolve(code ?? 0));
@@ -8502,8 +9631,17 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
8502
9631
  let child;
8503
9632
  try {
8504
9633
  const spawnImpl = opts.spawn || spawn;
8505
- const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env);
8506
- child = spawnImpl(bin, buildArgs(kind, systemPrompt, prompt, opts.permission), {
9634
+ const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env, {
9635
+ permission: opts.permission,
9636
+ mcpServers: [],
9637
+ mcpAllowlistMode: kind === "gemini" ? "exact" : undefined,
9638
+ });
9639
+ child = spawnImpl(bin, buildArgs(kind, systemPrompt, prompt, opts.permission, {
9640
+ model: opts.model,
9641
+ effort: opts.effort,
9642
+ authorityMode: opts.authorityMode,
9643
+ noToolsPolicyPath: opts.noToolsPolicyPath,
9644
+ }), {
8507
9645
  cwd,
8508
9646
  stdio: ["ignore", "pipe", "pipe"],
8509
9647
  env,
@@ -8614,7 +9752,11 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
8614
9752
  finishReject(new Error(`${kind} exited ${code}: ${stderr.slice(-500)}`));
8615
9753
  return;
8616
9754
  }
8617
- finishResolve(stdout.trim() || stderr.trim());
9755
+ const raw = stdout.trim() || stderr.trim();
9756
+ const captured = kind === "codex" && opts.authorityMode === "no-authority"
9757
+ ? codexCaptureAgentText(raw)
9758
+ : "";
9759
+ finishResolve(captured || raw);
8618
9760
  };
8619
9761
  onAbort = () => {
8620
9762
  const reason = opts.signal && opts.signal.reason;
@@ -8644,10 +9786,12 @@ function parity() {
8644
9786
  captureRuntime,
8645
9787
  runApi,
8646
9788
  resolveRuntime,
9789
+ listAvailableRuntimes,
8647
9790
  buildChildEnvCli,
8648
9791
  projectCwd,
8649
9792
  runCwd,
8650
9793
  userDataDir,
9794
+ modelRoutingReceiptPath: () => path.join(userDataDir(), "model-routing-receipts.jsonl"),
8651
9795
  resolveAgent,
8652
9796
  resolveFirm,
8653
9797
  listAgents,
@@ -8666,6 +9810,99 @@ function parity() {
8666
9810
  return parity._i;
8667
9811
  }
8668
9812
 
9813
+ // Agent Workforce Ontology is a separate, fail-closed route. Unlike the
9814
+ // compatibility router it gives final staffing authority to the active host
9815
+ // LLM and uses Hub MCP only for retrieval, validation, and pinned preparation.
9816
+ async function listWorkforceToolsCli({ db, roster, runtimeId, cwd, env, timeoutMs, signal }) {
9817
+ const mcp = require("./agentlas-experience-mcp.cjs");
9818
+ const servers = mcp.readConsentedSystemMcpServers(db, {
9819
+ userDataDir: userDataDir(),
9820
+ createRuntimeHome: false,
9821
+ }).slice(0, 8);
9822
+ if (!servers.length) return [];
9823
+ const deadline = Date.now() + Math.max(50, Math.min(12_000, Number(timeoutMs) || 12_000));
9824
+ const outcomes = new Array(servers.length);
9825
+ let cursor = 0;
9826
+ const worker = async () => {
9827
+ while (true) {
9828
+ const index = cursor++;
9829
+ if (index >= servers.length) return;
9830
+ const remaining = deadline - Date.now();
9831
+ if (remaining <= 0 || signal?.aborted) return;
9832
+ outcomes[index] = await mcp.probeSystemMcpServerConnection(servers[index], {
9833
+ cwd,
9834
+ env,
9835
+ userDataDir: userDataDir(),
9836
+ timeoutMs: Math.min(4_000, remaining),
9837
+ signal,
9838
+ });
9839
+ }
9840
+ };
9841
+ await Promise.all(Array.from({ length: Math.min(3, servers.length) }, () => worker()));
9842
+
9843
+ const safeId = /^[A-Za-z0-9][A-Za-z0-9_.$:/@+~-]{0,127}$/;
9844
+ const rows = [];
9845
+ for (let index = 0; index < servers.length; index += 1) {
9846
+ const server = servers[index];
9847
+ const listed = outcomes[index];
9848
+ if (!listed?.connected || !Array.isArray(listed.tools)) continue;
9849
+ for (const tool of listed.tools.slice(0, 256)) {
9850
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) continue;
9851
+ const declared = tool._meta?.agentlas || {};
9852
+ const toolId = typeof declared.toolId === "string" ? declared.toolId : String(tool.name || "");
9853
+ const capabilityIds = Array.isArray(declared.capabilityIds)
9854
+ ? [...new Set(declared.capabilityIds.filter((id) => /^[A-Za-z0-9][A-Za-z0-9._:/@-]{1,255}$/.test(String(id))))]
9855
+ : [];
9856
+ if (!safeId.test(toolId) || !capabilityIds.length) continue;
9857
+ const schemaJson = JSON.stringify(tool.inputSchema || {}, Object.keys(tool.inputSchema || {}).sort());
9858
+ for (const pinned of roster || []) {
9859
+ if (pinned?.permissionPolicy?.mcp?.mode !== "allowlist" || !pinned.permissionPolicy.mcp.allowedTools.includes(toolId)) continue;
9860
+ rows.push({
9861
+ slotId: pinned.slotId,
9862
+ agentReleaseId: pinned.agentReleaseId,
9863
+ permissionPolicyDigest: pinned.permissionPolicyDigest,
9864
+ provider: "mcp",
9865
+ toolId,
9866
+ serverId: server.id,
9867
+ description: "Ready consented host MCP tool",
9868
+ inputSchemaDigest: `sha256:${crypto.createHash("sha256").update(schemaJson).digest("hex")}`,
9869
+ // Terminal's one-shot native/API runners do not yet expose a proven
9870
+ // exact per-tool attachment boundary. Preserve the real tools/list
9871
+ // observation, but advertise no executable runtime instead of
9872
+ // manufacturing authority. collectToolInventory filters this row and
9873
+ // fails closed before the planner for a required capability.
9874
+ runtimeIds: [],
9875
+ selectiveEnforcement: "unavailable",
9876
+ capabilityIds,
9877
+ status: "observed-not-executable",
9878
+ });
9879
+ }
9880
+ }
9881
+ }
9882
+ return rows;
9883
+ }
9884
+
9885
+ function workforce() {
9886
+ if (!workforce._i) {
9887
+ workforce._i = require("./agentlas-workforce.cjs").create({
9888
+ captureRuntime,
9889
+ runApi,
9890
+ resolveRuntime,
9891
+ buildChildEnv: buildChildEnvCli,
9892
+ projectCwd,
9893
+ userDataDir,
9894
+ receiptFile: () => path.join(userDataDir(), "workforce-execution-receipts.jsonl"),
9895
+ cloudSessionCookie: cloudSessionCookieCli,
9896
+ fetchHub: (url, init) => fetchHubCli(url, init),
9897
+ listWorkforceTools: listWorkforceToolsCli,
9898
+ supportsWorkforceToolAuthority: async () => false,
9899
+ prefsLang,
9900
+ out,
9901
+ });
9902
+ }
9903
+ return workforce._i;
9904
+ }
9905
+
8669
9906
  // ── 명령 구현 ──────────────────────────────────────────────
8670
9907
  function cmdList(db) {
8671
9908
  const agents = listAgents(db);
@@ -8717,17 +9954,17 @@ function writeIfMissing(file, content) {
8717
9954
 
8718
9955
  function cmdCd(db, query) {
8719
9956
  const agent = resolveAgent(db, query);
8720
- if (!agent) fail(`에이전트를 찾을 없습니다: ${query}`);
9957
+ if (!agent) fail(`Agent not found: ${query}`);
8721
9958
  const folder = agentFolder(agent);
8722
9959
  ensureNativeFiles(agent, folder);
8723
9960
  // 경로만 stdout으로 (cd "$(agentlas cd seo)") — 안내는 stderr로.
8724
- process.stderr.write(`# ${agent.name} — 네이티브 CLI 컨텍스트(CLAUDE.md/AGENTS.md/GEMINI.md) 준비됨\n`);
9961
+ process.stderr.write(`# ${agent.name} — native CLI context ready (CLAUDE.md/AGENTS.md/GEMINI.md)\n`);
8725
9962
  process.stdout.write(folder + "\n");
8726
9963
  }
8727
9964
 
8728
9965
  function parseRunExperienceArgs(args) {
8729
9966
  const prompt = [];
8730
- const experience = { taskSignatures: [], environmentTags: [] };
9967
+ const experience = { taskSignatures: [], declaredTaskClasses: [], environmentTags: [], experiencePackReleaseIds: [] };
8731
9968
  let passthrough = false;
8732
9969
  const addList = (target, value) => {
8733
9970
  for (const item of String(value || "").split(",").map((entry) => entry.trim()).filter(Boolean)) {
@@ -8741,31 +9978,103 @@ function parseRunExperienceArgs(args) {
8741
9978
  const take = () => index + 1 < args.length ? String(args[++index]) : "";
8742
9979
  if (token === "--experience-base-release") experience.baseAgentReleaseId = take();
8743
9980
  else if (token.startsWith("--experience-base-release=")) experience.baseAgentReleaseId = token.slice(26);
9981
+ else if (token === "--experience-pack-release") addList(experience.experiencePackReleaseIds, take());
9982
+ else if (token.startsWith("--experience-pack-release=")) addList(experience.experiencePackReleaseIds, token.slice(26));
8744
9983
  else if (token === "--experience-agent-definition") experience.agentDefinitionId = take();
8745
9984
  else if (token.startsWith("--experience-agent-definition=")) experience.agentDefinitionId = token.slice(30);
8746
9985
  else if (token === "--experience-task-signature") addList(experience.taskSignatures, take());
8747
9986
  else if (token.startsWith("--experience-task-signature=")) addList(experience.taskSignatures, token.slice(28));
9987
+ else if (token === "--experience-task-class") addList(experience.declaredTaskClasses, take());
9988
+ else if (token.startsWith("--experience-task-class=")) addList(experience.declaredTaskClasses, token.slice(24));
8748
9989
  else if (token === "--experience-environment") addList(experience.environmentTags, take());
8749
9990
  else if (token.startsWith("--experience-environment=")) addList(experience.environmentTags, token.slice(25));
9991
+ else if (token === "--experience-desktop-loadout") experience.desktopLoadout = true;
9992
+ else if (
9993
+ token === "--experience-loadout" || token === "--experience-loadout-file" ||
9994
+ token.startsWith("--experience-loadout=") || token.startsWith("--experience-loadout-file=")
9995
+ ) {
9996
+ throw new Error("Custom Experience loadout paths are no longer supported; use --experience-desktop-loadout.");
9997
+ }
8750
9998
  else if (token === "--no-experience") experience.disabled = true;
8751
9999
  else prompt.push(token);
8752
10000
  }
8753
10001
  return { prompt: prompt.join(" "), experience };
8754
10002
  }
8755
10003
 
10004
+ function resolveRuntimeExperienceCli(agent, prompt, requested, cwd, overrides = {}) {
10005
+ const prepared = desktopOntologyLoadout.prepareDesktopLoadoutRequest({
10006
+ db: overrides.db,
10007
+ agent,
10008
+ userDataDir: overrides.userDataDir || userDataDir(),
10009
+ requested: requested || {},
10010
+ now: overrides.now,
10011
+ });
10012
+ if (prepared.mode === "skip") {
10013
+ return { disabled: true, observableReason: prepared.reason, resolution: "skipped" };
10014
+ }
10015
+ const resolved = terminalExperienceExchange.resolveRuntimeExperienceForAgent({
10016
+ userDataDir: overrides.userDataDir || userDataDir(),
10017
+ cwd,
10018
+ prompt,
10019
+ requested: prepared.requested || requested || {},
10020
+ agent,
10021
+ agentRoot: agent ? (overrides.agentRoot || agentFolder(agent)) : null,
10022
+ ...(overrides.platform ? { platform: overrides.platform } : {}),
10023
+ ...(overrides.arch ? { arch: overrides.arch } : {}),
10024
+ ...(overrides.runtime ? { runtime: overrides.runtime } : {}),
10025
+ });
10026
+ if (prepared.mode !== "resolved") return resolved;
10027
+ const authority = prepared.authority;
10028
+ const tasteRuntime = {
10029
+ tasteRuntimeOverlay: authority.tasteRuntimeOverlay || null,
10030
+ loadoutAuthority: "desktop-terminal-exact-loadout",
10031
+ projectionRevision: authority.projectionRevision,
10032
+ loadoutRevision: authority.loadoutRevision,
10033
+ };
10034
+ if (!authority.experiencePackReleaseId) {
10035
+ return {
10036
+ disabled: true,
10037
+ resolution: "desktop-loadout-taste-only",
10038
+ ...tasteRuntime,
10039
+ };
10040
+ }
10041
+ if (resolved.disabled === true) return { ...resolved, ...tasteRuntime };
10042
+ if (
10043
+ resolved.agentDefinitionId !== authority.agentDefinitionId ||
10044
+ resolved.baseAgentReleaseId !== authority.baseAgentReleaseId ||
10045
+ !Array.isArray(resolved.experiencePackReleaseIds) ||
10046
+ resolved.experiencePackReleaseIds.length !== 1 ||
10047
+ resolved.experiencePackReleaseIds[0] !== authority.experiencePackReleaseId
10048
+ ) {
10049
+ return {
10050
+ disabled: true,
10051
+ observableReason: "desktop-loadout-runtime-resolution-mismatch",
10052
+ resolution: "skipped",
10053
+ ...tasteRuntime,
10054
+ };
10055
+ }
10056
+ return {
10057
+ ...resolved,
10058
+ ...tasteRuntime,
10059
+ };
10060
+ }
10061
+
8756
10062
  async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = null) {
8757
10063
  const agent = resolveAgent(db, query);
8758
10064
  if (!agent) {
8759
10065
  const routedPrompt = [query, prompt].filter(Boolean).join(" ").trim() || (await readStdin());
8760
- if (!routedPrompt || !routedPrompt.trim()) fail("프롬프트가 비어 있습니다. agentlas run <agent> \"...\" 또는 agentlas run \"...\" 형식으로 입력하세요.");
10066
+ if (!routedPrompt || !routedPrompt.trim()) fail("Prompt is empty. Use agentlas run <agent> \"...\" or agentlas run \"...\".");
8761
10067
  return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride, runtimeExperience);
8762
10068
  }
8763
10069
  let userPrompt = prompt;
8764
10070
  if (!userPrompt) userPrompt = await readStdin();
8765
- if (!userPrompt || !userPrompt.trim()) fail("프롬프트가 비어 있습니다. agentlas run <agent> \"...\" 또는 stdin으로 전달하세요.");
10071
+ if (!userPrompt || !userPrompt.trim()) fail("Prompt is empty. Pass agentlas run <agent> \"...\" or provide it through stdin.");
8766
10072
  process.stderr.write(`▸ ${agent.name}\n`);
10073
+ const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-run");
10074
+ const cwd = projectPath || projectCwd();
10075
+ const resolvedExperience = resolveRuntimeExperienceCli(agent, userPrompt.trim(), runtimeExperience, cwd, { db });
8767
10076
  const code = await executeOnce(db, agentSystemPromptCli(agent), userPrompt.trim(), runtimeOverride, {
8768
- projectPath: activeProjectPath(db), agentId: agent.id, permission: PERMISSION, runtimeExperience,
10077
+ projectPath, agentId: agent.id, permission: PERMISSION, runtimeExperience: resolvedExperience,
8769
10078
  });
8770
10079
  process.exit(code);
8771
10080
  }
@@ -8773,28 +10082,34 @@ async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = nu
8773
10082
  async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null) {
8774
10083
  const lang = prefsLang();
8775
10084
  const choice = autoRouteAgent(db, prompt, lang);
8776
- if (!choice) fail("자동 라우팅할 에이전트가 없습니다. agentlas list로 설치 상태를 확인하세요.");
10085
+ if (!choice) fail("No agent is available for automatic routing. Check installation with agentlas list.");
8777
10086
  if (choice.direct) {
8778
10087
  // 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
8779
10088
  process.stderr.write(`▸ direct (no agent)\n`);
8780
10089
  process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
8781
10090
  const sys = `${autoRoutePreamble(choice, lang)}\n\n${directSystemPrompt(lang)}`;
10091
+ const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-auto-run");
10092
+ const cwd = projectPath || projectCwd();
10093
+ const resolvedExperience = resolveRuntimeExperienceCli(null, prompt.trim(), runtimeExperience, cwd, { db });
8782
10094
  const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
8783
- projectPath: activeProjectPath(db),
10095
+ projectPath,
8784
10096
  agentId: null,
8785
10097
  permission: PERMISSION,
8786
- runtimeExperience,
10098
+ runtimeExperience: resolvedExperience,
8787
10099
  });
8788
10100
  process.exit(code);
8789
10101
  }
8790
10102
  process.stderr.write(`▸ ${choice.agent.name} (auto)\n`);
8791
10103
  process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
8792
10104
  const sys = `${autoRoutePreamble(choice, lang)}\n\n${agentSystemPromptCli(choice.agent)}`;
10105
+ const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-auto-run");
10106
+ const cwd = projectPath || projectCwd();
10107
+ const resolvedExperience = resolveRuntimeExperienceCli(choice.agent, prompt.trim(), runtimeExperience, cwd, { db });
8793
10108
  const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
8794
- projectPath: activeProjectPath(db),
10109
+ projectPath,
8795
10110
  agentId: choice.agent.id,
8796
10111
  permission: PERMISSION,
8797
- runtimeExperience,
10112
+ runtimeExperience: resolvedExperience,
8798
10113
  });
8799
10114
  process.exit(code);
8800
10115
  }
@@ -8802,7 +10117,7 @@ async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null)
8802
10117
  // chat / open / 에이전트명 단독 → 네이티브 CLI 대화형 세션 (claude처럼 바로 접속)
8803
10118
  function cmdOpen(db, query, runtimeOverride) {
8804
10119
  const agent = resolveAgent(db, query);
8805
- if (!agent) fail(`에이전트를 찾을 없습니다: ${query}`);
10120
+ if (!agent) fail(`Agent not found: ${query}`);
8806
10121
  launchInteractive(db, agent, runtimeOverride);
8807
10122
  }
8808
10123
 
@@ -8837,15 +10152,37 @@ function firmSystemPrompt(db, firm) {
8837
10152
  /* ignore */
8838
10153
  }
8839
10154
  const base = (ceo && ceo.system_prompt) || `You are the CEO of ${firm.name}.`;
8840
- return `${base}\n\n[FIRM] 당신은 '${firm.name}' 회사의 CEO입니다. 사용자 명령을 부서에 위임해 처리하세요.\n조직도:\n${roster}`;
10155
+ return `${base}\n\n[FIRM] You are the CEO of '${firm.name}'. Delegate user requests to the appropriate departments.\nOrganization:\n${roster}`;
8841
10156
  }
8842
10157
  async function cmdFirm(db, query, prompt, runtimeOverride) {
8843
10158
  const firm = resolveFirm(db, query);
8844
- if (!firm) fail(`회사를 찾을 없습니다: ${query}`);
10159
+ if (!firm) fail(`Company not found: ${query}`);
8845
10160
  const sys = firmSystemPrompt(db, firm);
8846
10161
  if (prompt && prompt.trim()) {
8847
10162
  process.stderr.write(`▸ ${firm.name} CEO\n`);
8848
- const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, { projectPath: activeProjectPath(db), agentId: firm.ceo_agent_id, permission: PERMISSION });
10163
+ const runtime = resolveRuntime(db, runtimeOverride);
10164
+ const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-firm-run");
10165
+ const allocated = await allocateSingleWorkloadCli(db, prompt.trim(), {
10166
+ runtime,
10167
+ cwd: projectCwd(),
10168
+ projectPath,
10169
+ agentId: firm.ceo_agent_id,
10170
+ lang: prefsLang(),
10171
+ mode: "team",
10172
+ onWarning: (message) => process.stderr.write(`▸ ${message}\n`),
10173
+ });
10174
+ process.stderr.write(
10175
+ `▸ team model route · ${allocated.resolution.source} · ${allocated.resolution.model || runtime.kind || runtime.backend}` +
10176
+ `${allocated.resolution.effort ? ` · ${allocated.resolution.effort}` : ""}` +
10177
+ `${allocated.resolution.fallbackReason ? ` · ${allocated.resolution.fallbackReason}` : ""}\n`,
10178
+ );
10179
+ const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
10180
+ projectPath,
10181
+ agentId: firm.ceo_agent_id,
10182
+ permission: PERMISSION,
10183
+ model: allocated.resolution.model,
10184
+ effort: allocated.resolution.effort,
10185
+ });
8849
10186
  process.exit(code);
8850
10187
  }
8851
10188
  // 대화형 — agentlas TUI. CEO 페르소나를 system으로, 작업은 현재 폴더에서.
@@ -9037,23 +10374,23 @@ async function cmdEnv(db) {
9037
10374
  ...readDotEnvFileCli(path.join(os.homedir(), ".agentlas", "credentials.env")),
9038
10375
  };
9039
10376
  const keys = Object.keys(fromFiles).sort();
9040
- out(`공유 env ${keys.length} (값은 표시 함, credentials.env 기준):`);
10377
+ out(`Shared env keys: ${keys.length} (values hidden; from credentials.env):`);
9041
10378
  for (const k of keys) out(` ${k}`);
9042
10379
  out("");
9043
- out("키체인 저장 키는 데스크탑 앱의 설정자격증명에서 보입니다.");
10380
+ out("Keychain entries are available in Desktop settings Credentials.");
9044
10381
  return;
9045
10382
  }
9046
10383
  const keytar = readKeytar();
9047
- if (!keytar) fail("keytar 모듈을 불러올 없습니다( 런타임으로 실행 필요).");
10384
+ if (!keytar) fail("The keytar module is unavailable (run through the app runtime).");
9048
10385
  let creds;
9049
10386
  try {
9050
10387
  creds = await keytar.findCredentials(SERVICE);
9051
10388
  } catch (e) {
9052
- fail("env 조회 실패: " + ((e && e.message) || e));
10389
+ fail("Failed to read environment settings: " + ((e && e.message) || e));
9053
10390
  return;
9054
10391
  }
9055
10392
  const keys = creds.map((c) => c.account).filter((a) => a.startsWith(ENV_PREFIX)).map((a) => a.slice(ENV_PREFIX.length));
9056
- out(`공유 env ${keys.length} (값은 표시 안 함):`);
10393
+ out(`Shared env keys: ${keys.length} (values hidden):`);
9057
10394
  for (const k of keys.sort()) out(` ${k}`);
9058
10395
  }
9059
10396
 
@@ -9082,7 +10419,7 @@ function setMultimodalCli(db, modality, providerId) {
9082
10419
  const mm = loadMultimodalCatalog();
9083
10420
  if (!["image", "video", "audio"].includes(modality)) fail("usage: agentlas multimodal set <image|video|audio> <provider-id>");
9084
10421
  const provider = mm.MULTIMODAL_PROVIDERS.find((p) => p.id === providerId && p.modality === modality);
9085
- if (!provider) fail(`provider를 찾을 없습니다: ${providerId} (${modality})`);
10422
+ if (!provider) fail(`Provider not found: ${providerId} (${modality})`);
9086
10423
  const key = modality === "image" ? "imageProvider" : modality === "video" ? "videoProvider" : "audioProvider";
9087
10424
  return saveMultimodalSettingsCli(db, { [key]: providerId });
9088
10425
  }
@@ -9145,7 +10482,7 @@ function parseUpdateFlags(args) {
9145
10482
  else if (arg === "--no-launch") flags.launch = false;
9146
10483
  else if (arg === "--url") flags.url = args[++i] || flags.url;
9147
10484
  else if (arg === "--help" || arg === "-h" || arg === "help") flags.help = true;
9148
- else fail(`알 수 없는 update 옵션: ${arg}`);
10485
+ else fail(`Unknown update option: ${arg}`);
9149
10486
  }
9150
10487
  return flags;
9151
10488
  }
@@ -9220,19 +10557,19 @@ function updateTimeoutError(kind, ms) {
9220
10557
  return updateTransferError(`AGENTLAS_UPDATE_${kind.toUpperCase()}_TIMEOUT`, message);
9221
10558
  }
9222
10559
 
9223
- function parseSafeUpdateUrl(value, label = "업데이트 URL") {
10560
+ function parseSafeUpdateUrl(value, label = "update URL") {
9224
10561
  let parsed;
9225
10562
  try {
9226
10563
  parsed = new URL(String(value || ""));
9227
10564
  } catch (error) {
9228
- throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} 형식이 올바르지 않습니다.`, error);
10565
+ throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} has an invalid format.`, error);
9229
10566
  }
9230
10567
  const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
9231
10568
  if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback)) {
9232
- throw updateTransferError("AGENTLAS_UPDATE_INSECURE_URL", `${label} HTTPS여야 합니다(로컬 루프백 제외).`);
10569
+ throw updateTransferError("AGENTLAS_UPDATE_INSECURE_URL", `${label} must use HTTPS (except local loopback).`);
9233
10570
  }
9234
10571
  if (parsed.username || parsed.password) {
9235
- throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} 사용자 정보가 포함될 수 없습니다.`);
10572
+ throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} cannot contain user information.`);
9236
10573
  }
9237
10574
  return parsed.toString();
9238
10575
  }
@@ -9379,7 +10716,7 @@ async function fetchDesktopRelease(url) {
9379
10716
  return await fetchUpdateMetadata(url, { signal: controller.signal });
9380
10717
  } catch (error) {
9381
10718
  const message = String((error && error.message) || error);
9382
- fail(`업데이트 확인 실패: ${message}`);
10719
+ fail(`Update check failed: ${message}`);
9383
10720
  }
9384
10721
  }
9385
10722
 
@@ -9418,19 +10755,19 @@ async function cmdUpdateStandalone(flags) {
9418
10755
  if (flags.json) {
9419
10756
  return out(JSON.stringify({ currentVersion, latestVersion, updateAvailable: comparison == null ? null : comparison < 0, channel: "npm" }, null, 2));
9420
10757
  }
9421
- out(`현재 버전: ${currentVersion}`);
10758
+ out(`Current version: ${currentVersion}`);
9422
10759
  if (!latestVersion) {
9423
- out("npm 레지스트리에서 최신 버전을 확인하지 못했습니다 (오프라인이거나 아직 미발행).");
9424
- out("수동 업데이트: npm i -g agentlas@latest");
10760
+ out("Could not check the latest version on the npm registry (offline or not published yet).");
10761
+ out("Manual update: npm i -g agentlas@latest");
9425
10762
  return;
9426
10763
  }
9427
- out(`최신 버전: ${latestVersion}`);
10764
+ out(`Latest version: ${latestVersion}`);
9428
10765
  if (comparison == null) {
9429
- out("버전 형식을 비교하지 못했습니다. 수동 업데이트: npm i -g agentlas@latest");
10766
+ out("Could not compare version formats. Manual update: npm i -g agentlas@latest");
9430
10767
  } else if (comparison < 0) {
9431
- out("업데이트: npm i -g agentlas@latest");
10768
+ out("Update: npm i -g agentlas@latest");
9432
10769
  } else {
9433
- out("이미 최신 버전입니다.");
10770
+ out("Already on the latest version.");
9434
10771
  }
9435
10772
  }
9436
10773
 
@@ -9445,7 +10782,7 @@ async function cmdUpdate(args) {
9445
10782
  const latestVersion = String(release.version || "");
9446
10783
  const artifact = findCurrentArtifact(release);
9447
10784
  const comparison = compareSemVer(currentVersion, latestVersion);
9448
- if (comparison == null) fail(`현재/최신 버전이 SemVer 형식이 아닙니다: current=${currentVersion} latest=${latestVersion}`);
10785
+ if (comparison == null) fail(`Current/latest version is not valid SemVer: current=${currentVersion} latest=${latestVersion}`);
9449
10786
  const updateAvailable = comparison < 0;
9450
10787
  const status = {
9451
10788
  currentVersion,
@@ -9463,15 +10800,15 @@ async function cmdUpdate(args) {
9463
10800
  if (flags.json) return out(JSON.stringify(status, null, 2));
9464
10801
  out(formatUpdateSummary(status));
9465
10802
  if (flags.check) return;
9466
- if (release.ready !== true) fail("최신 릴리즈가 아직 공개 설치 가능 상태가 아닙니다.");
9467
- if (!updateAvailable && !flags.force) return out("이미 최신 버전입니다.");
9468
- if (process.platform !== "darwin") return out(" OS는 아직 자동 설치를 지원하지 않습니다. 릴리즈/다운로드 링크에서 최신 설치 파일을 받으세요.");
9469
- if (!artifact || !artifact.url) fail("현재 Mac에 맞는 DMG 찾지 못했습니다.");
10803
+ if (release.ready !== true) fail("The latest release is not ready for public installation.");
10804
+ if (!updateAvailable && !flags.force) return out("Already on the latest version.");
10805
+ if (process.platform !== "darwin") return out("Automatic installation is not supported on this OS yet. Use the release/download link above.");
10806
+ if (!artifact || !artifact.url) fail("Could not find a DMG for this Mac.");
9470
10807
  await installMacDesktopUpdate(release, artifact, flags);
9471
10808
  }
9472
10809
 
9473
10810
  function requirePath(commandPath, label) {
9474
- if (!fs.existsSync(commandPath)) fail(`업데이트에 필요한 도구가 없습니다: ${label}`);
10811
+ if (!fs.existsSync(commandPath)) fail(`Required update tool not found: ${label}`);
9475
10812
  return commandPath;
9476
10813
  }
9477
10814
 
@@ -9807,9 +11144,9 @@ async function installMacDesktopUpdate(release, artifact, flags) {
9807
11144
  const stagingPath = path.join(targetDir, `.${targetName}.installing.${transactionId}.app`);
9808
11145
 
9809
11146
  try {
9810
- out(`다운로드: ${fileName}`);
11147
+ out(`Download: ${fileName}`);
9811
11148
  await downloadUpdateFile(validatedArtifact.url, dmgPath, validatedArtifact);
9812
- out("검증: DMG, notarization, Gatekeeper");
11149
+ out("Verify: DMG, notarization, Gatekeeper");
9813
11150
  await runCommand(hdiutil, ["verify", dmgPath]);
9814
11151
  await runCommand(xcrun, ["stapler", "validate", dmgPath]);
9815
11152
  await runCommand(spctl, ["-a", "-t", "open", "--context", "context:primary-signature", "-vv", dmgPath]);
@@ -9818,16 +11155,16 @@ async function installMacDesktopUpdate(release, artifact, flags) {
9818
11155
  mountPoint = parseHdiutilMountPoint(mount.stdout);
9819
11156
  const sourceApp = mountPoint ? path.join(mountPoint, "Agentlas.app") : "";
9820
11157
  if (!sourceApp || !fs.existsSync(sourceApp)) {
9821
- throw updateTransferError("AGENTLAS_UPDATE_APP_MISSING", "DMG 안에서 Agentlas.app 찾지 못했습니다.");
11158
+ throw updateTransferError("AGENTLAS_UPDATE_APP_MISSING", "Agentlas.app was not found in the DMG.");
9822
11159
  }
9823
11160
 
9824
11161
  const installedVersion = await runCommand(plistBuddy, ["-c", "Print :CFBundleShortVersionString", path.join(sourceApp, "Contents", "Info.plist")], { capture: true });
9825
11162
  const appVersion = installedVersion.stdout.trim();
9826
11163
  if (appVersion !== String(release.version)) {
9827
- throw updateTransferError("AGENTLAS_UPDATE_VERSION_MISMATCH", `앱 버전이 릴리즈와 다릅니다: release=${release.version} app=${appVersion}`);
11164
+ throw updateTransferError("AGENTLAS_UPDATE_VERSION_MISMATCH", `App version does not match the release: release=${release.version} app=${appVersion}`);
9828
11165
  }
9829
11166
 
9830
- out("설치: 기존 Agentlas 종료 교체");
11167
+ out("Install: quit the existing Agentlas app and replace it");
9831
11168
  await runCommand(osascript, ["-e", 'tell application "Agentlas" to quit'], { capture: true, allowFailure: true });
9832
11169
  await sleep(2_000);
9833
11170
  const replacement = await replaceMacAppBundle({
@@ -9838,10 +11175,10 @@ async function installMacDesktopUpdate(release, artifact, flags) {
9838
11175
  runCommand,
9839
11176
  commands: { codesign, spctl, ditto, mv, rm },
9840
11177
  });
9841
- if (replacement.backupRetained) out(`주의: 검증된 앱은 설치됐지만 이전 백업을 지우지 못했습니다: ${replacement.backupPath}`);
11178
+ if (replacement.backupRetained) out(`Warning: the verified app was installed, but the previous app backup could not be removed: ${replacement.backupPath}`);
9842
11179
  if (fs.existsSync(lsregister)) await runCommand(lsregister, ["-f", targetApp], { allowFailure: true });
9843
11180
  if (flags.launch) await runCommand(open, ["-a", "Agentlas"], { allowFailure: true });
9844
- out(`Agentlas ${release.version} 설치 완료.`);
11181
+ out(`Agentlas ${release.version} installed.`);
9845
11182
  } finally {
9846
11183
  if (mountPoint) await runCommand(hdiutil, ["detach", mountPoint], { allowFailure: true });
9847
11184
  fs.rmSync(tmpDir, { recursive: true, force: true });
@@ -9933,32 +11270,32 @@ function oberonScaffold(args) {
9933
11270
  };
9934
11271
  if (flags.titles) manifest.titles = oberonSampleTitles(title);
9935
11272
  fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2), "utf8");
9936
- out(`✓ 매니페스트 생성: ${outPath}`);
9937
- out(` · ${shotCount} · ${aspect} · ${manifest.provider}`);
9938
- out(` · 프롬프트를 채운 뒤: agentlas oberon render ${path.basename(outPath)}`);
9939
- out(` · 또는 에이전트로 채우기: agentlas run oberon-film-studio "30 향수 광고 트레일러"`);
9940
- if (!flags.titles) out(` · 타이틀/자막 번인 샘플 포함하려면 --titles 플래그`);
11273
+ out(`✓ Manifest created: ${outPath}`);
11274
+ out(` · ${shotCount} shots · ${aspect} · ${manifest.provider}`);
11275
+ out(` · fill prompts, then run: agentlas oberon render ${path.basename(outPath)}`);
11276
+ out(` · or use an agent: agentlas run oberon-film-studio "30-second fragrance ad trailer"`);
11277
+ if (!flags.titles) out(` · use --titles to include title/subtitle burn-in samples`);
9941
11278
  }
9942
11279
 
9943
11280
  function oberonRender(args) {
9944
11281
  const { flags, rest } = oberonParseFlags(args);
9945
- if (!rest[0]) fail("렌더할 매니페스트 경로가 필요합니다: agentlas oberon render <manifest.json>");
11282
+ if (!rest[0]) fail("A manifest path is required: agentlas oberon render <manifest.json>");
9946
11283
  const manifestPath = path.resolve(rest[0]);
9947
- if (!fs.existsSync(manifestPath)) fail(`매니페스트를 찾을 없습니다: ${manifestPath}`);
11284
+ if (!fs.existsSync(manifestPath)) fail(`Manifest not found: ${manifestPath}`);
9948
11285
 
9949
11286
  let manifest;
9950
11287
  try {
9951
11288
  manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
9952
11289
  } catch (e) {
9953
- return fail(`매니페스트 JSON 파싱 실패: ${e.message}`);
11290
+ return fail(`Failed to parse manifest JSON: ${e.message}`);
9954
11291
  }
9955
- if (!Array.isArray(manifest.shots) || !manifest.shots.length) fail("매니페스트에 shots[] 가 비어 있습니다.");
11292
+ if (!Array.isArray(manifest.shots) || !manifest.shots.length) fail("The manifest has no shots[].");
9956
11293
 
9957
11294
  const root = oberonRepoRoot();
9958
11295
  const script = path.join(root, "scripts", "render-oberon-live-request.cjs");
9959
11296
  const builtRender = path.join(root, "dist", "electron", "oberon", "render.js");
9960
- if (!fs.existsSync(script)) fail(`헤드리스 렌더 스크립트가 없습니다(패키지 앱에는 미포함): ${script}`);
9961
- if (!fs.existsSync(builtRender)) fail(`Electron 빌드가 필요합니다. 먼저: npm run build:electron (없는 파일: ${builtRender})`);
11297
+ if (!fs.existsSync(script)) fail(`Headless render script not found (not included in the packaged app): ${script}`);
11298
+ if (!fs.existsSync(builtRender)) fail(`An Electron build is required. Run npm run build:electron first (missing: ${builtRender})`);
9962
11299
 
9963
11300
  // --max-shots 등 오버라이드가 있으면 사용자 매니페스트는 그대로 두고 임시 패치본을 만든다.
9964
11301
  let reqPath = manifestPath;
@@ -9983,18 +11320,18 @@ function oberonRender(args) {
9983
11320
  if (flags["poll-ms"]) childEnv.OBERON_LIVE_POLL_MS = String(flags["poll-ms"]);
9984
11321
  if (flags.open) childEnv.OBERON_LIVE_OPEN_DELIVERY = "1";
9985
11322
 
9986
- out(`▶ Oberon 렌더: "${manifest.title}" (${manifest.shots.length}샷, 최대 ${overrides.maxShots ?? manifest.maxShots ?? 3})`);
9987
- out(` 매니페스트: ${manifestPath}`);
9988
- out(` 납품 폴더: ${deliveryDir}`);
9989
- if (manifest.titles) out(` 타이틀/자막 번인: 활성 → *_titled.mp4 추가 생성`);
11323
+ out(`▶ Oberon render: "${manifest.title}" (${manifest.shots.length} shots, max ${overrides.maxShots ?? manifest.maxShots ?? 3})`);
11324
+ out(` Manifest: ${manifestPath}`);
11325
+ out(` Delivery folder: ${deliveryDir}`);
11326
+ if (manifest.titles) out(` title/subtitle burn-in: enabledgenerating additional *_titled.mp4`);
9990
11327
 
9991
11328
  if (flags["dry-run"]) {
9992
- out("\n[dry-run] 실행할 명령:");
11329
+ out("\n[dry-run] Command to run:");
9993
11330
  out(` ${process.execPath} ${script}`);
9994
11331
  out(" env: OBERON_LIVE_VEO=1");
9995
11332
  out(` OBERON_LIVE_REQUEST_FILE=${reqPath}`);
9996
11333
  out(` OBERON_LIVE_DELIVERY_DIR=${deliveryDir}`);
9997
- out(" (full Electron · GEMINI_API_KEY/GOOGLE_CLOUD_PROJECT 볼트 필요)");
11334
+ out(" (full Electron · GEMINI_API_KEY/GOOGLE_CLOUD_PROJECT vault required)");
9998
11335
  return;
9999
11336
  }
10000
11337
 
@@ -10015,11 +11352,11 @@ function oberonRender(args) {
10015
11352
  child.stderr.on("data", (c) => process.stderr.write(c));
10016
11353
  child.on("close", (code) => {
10017
11354
  if (code === 0) {
10018
- out(`\n✓ 렌더 완료납품 폴더: ${deliveryDir}`);
11355
+ out(`\n✓ Render completedelivery folder: ${deliveryDir}`);
10019
11356
  const titled = files.filter((f) => f.kind && f.kind.startsWith("titled"));
10020
- if (titled.length) out(` 타이틀/자막 번인본: ${titled.map((f) => f.name).join(", ")}`);
11357
+ if (titled.length) out(` title/subtitle burn-in files: ${titled.map((f) => f.name).join(", ")}`);
10021
11358
  } else {
10022
- process.stderr.write(`\n✖ 렌더 실패 (exit ${code})\n`);
11359
+ process.stderr.write(`\n✖ Render failed (exit ${code})\n`);
10023
11360
  process.exitCode = code || 1;
10024
11361
  }
10025
11362
  resolve();
@@ -10077,7 +11414,7 @@ function slugifyOberon(value) {
10077
11414
  function oberonList() {
10078
11415
  const dir = path.join(userDataDir(), "oberon");
10079
11416
  if (!fs.existsSync(dir)) {
10080
- out("아직 렌더 산출물이 없습니다. agentlas oberon scaffold my.json 으로 시작하세요.");
11417
+ out("No render outputs yet. Start with agentlas oberon scaffold my.json.");
10081
11418
  return;
10082
11419
  }
10083
11420
  const entries = fs
@@ -10101,40 +11438,40 @@ function oberonList() {
10101
11438
  .sort((a, b) => b.mtime - a.mtime)
10102
11439
  .slice(0, 15);
10103
11440
  if (!entries.length) {
10104
- out("아직 렌더 산출물이 없습니다.");
11441
+ out("No render outputs yet.");
10105
11442
  return;
10106
11443
  }
10107
- out(`최근 Oberon 렌더 (${dir}):\n`);
11444
+ out(`Recent Oberon renders (${dir}):\n`);
10108
11445
  for (const e of entries) {
10109
11446
  const masters = e.files.filter((f) => /master|titled/.test(f) && /\.(mp4|mov)$/.test(f));
10110
11447
  const when = e.mtime ? new Date(e.mtime).toISOString().slice(0, 16).replace("T", " ") : "";
10111
11448
  out(` ${when} ${e.name}`);
10112
11449
  if (masters.length) out(` ${masters.join(", ")}`);
10113
11450
  }
10114
- out(`\n폴더 열기: agentlas oberon open`);
11451
+ out(`\nOpen the folder with: agentlas oberon open`);
10115
11452
  }
10116
11453
 
10117
11454
  function oberonOpen(args) {
10118
11455
  const target = args[0] ? path.resolve(args[0]) : path.join(userDataDir(), "oberon");
10119
- if (!fs.existsSync(target)) fail(`경로가 없습니다: ${target}`);
11456
+ if (!fs.existsSync(target)) fail(`Path not found: ${target}`);
10120
11457
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
10121
11458
  spawn(opener, [target], { detached: true, stdio: "ignore" }).unref();
10122
- out(`폴더 열기: ${target}`);
11459
+ out(`Opening folder: ${target}`);
10123
11460
  }
10124
11461
 
10125
11462
  function oberonHelp() {
10126
11463
  out(
10127
11464
  [
10128
- "agentlas oberon — 터미널에서 AI 필름 렌더",
11465
+ "agentlas oberon — AI film rendering from the terminal",
10129
11466
  "",
10130
11467
  " oberon scaffold [out.json] [--title T] [--aspect 16:9] [--shots N] [--titles]",
10131
- " 편집 가능한 렌더 매니페스트 생성 (--titles: 타이틀/자막 번인 샘플 포함)",
11468
+ " create an editable render manifest (--titles: include title/subtitle burn-in samples)",
10132
11469
  " oberon render <manifest.json> [--delivery DIR] [--max-shots N] [--open] [--dry-run]",
10133
- " full Electron 렌더 스폰 + 진행률 스트리밍 (GEMINI_API_KEY 볼트 필요)",
11470
+ " spawn full Electron render + stream progress (GEMINI_API_KEY vault required)",
10134
11471
  " oberon list 최근 렌더 산출물",
10135
11472
  " oberon open [path] 산출물 폴더 열기",
10136
11473
  "",
10137
- "프롬프트는 직접 채우거나, 에이전트에게: agentlas run oberon-film-studio \"30 향수 광고\"",
11474
+ "Fill prompts directly, or ask an agent: agentlas run oberon-film-studio \"30-second fragrance ad\"",
10138
11475
  ].join("\n"),
10139
11476
  );
10140
11477
  }
@@ -10158,7 +11495,7 @@ async function cmdOberon(args) {
10158
11495
  case "-h":
10159
11496
  return oberonHelp();
10160
11497
  default:
10161
- fail(`알 수 없는 oberon 하위명령: ${sub} (scaffold|render|list|open|help)`);
11498
+ fail(`Unknown oberon subcommand: ${sub} (scaffold|render|list|open|help)`);
10162
11499
  }
10163
11500
  }
10164
11501
 
@@ -10176,26 +11513,31 @@ function cmdHelp() {
10176
11513
  hdr("TALK & RUN"),
10177
11514
  " <agent> jump into a chat with one agent (e.g. agentlas seo)",
10178
11515
  " run [agent] [prompt] one-shot — omit agent to auto-route (reads stdin if no prompt)",
11516
+ " --experience-desktop-loadout use Desktop's fresh exact Operational/Taste loadout receipt",
11517
+ " --no-experience highest precedence; do not read or inject a loadout",
10179
11518
  " firm <firm> [cmd] delegate to a company's CEO (interactive if no cmd)",
10180
11519
  " chats [n] recent conversations · chat resume in REPL: /resume",
10181
11520
  "",
10182
11521
  hdr("AGENTS & HUB (Agentlas OS surface)"),
10183
11522
  " search \"<what you need>\" discover agents in the Hub + local (hep-search)",
10184
11523
  " install <slug> install an agent from the Hub (hep-cloud)",
11524
+ " plugin add <slug> install a Hub plugin (MCP servers) into this machine",
10185
11525
  " build \"<request>\" build/repair/package an agent or team (hep-build)",
10186
11526
  " upload <path> save owner-private in Agent Cloud (default) (hep-upload)",
10187
11527
  " --visibility marketplace explicit compatibility flag: publish to Hub",
10188
11528
  " connect [<sub>] wire Telegram / platforms to an agent team (hep-connect)",
10189
11529
  " import <path> import a local agent/team folder",
10190
11530
  " list installed agents/companies + active runtime",
10191
- " experience <sub> portable Experience: validate|save|publish|status|export|withdraw",
10192
- " legacy local intents: list|inspect|unpublish|legacy-publish",
11531
+ " experience <sub> portable Experience: list|inspect|validate|save|publish|status|export|unpublish",
11532
+ " legacy local intents require explicit legacy-* commands",
10193
11533
  " variant resolve local variant selection: selected|fallback|base-only|error",
10194
11534
  "",
10195
11535
  hdr("EXECUTE"),
10196
- " storm <goal> force-robust pipeline: routeverify → execute (Stormbreaker) [--research]",
11536
+ " storm <goal> Agentlas Goal+UltraCode harness: planallocate → execute → verify [--research]",
10197
11537
  " swarm <goal> emergent agent swarm — parallel workers + synthesizer [--parallel N]",
10198
- " network <request> decompose a request into an A2A task force (hep-network)",
11538
+ " network <request> host-LLM workforce ontology exact TF execute [--benchmark]",
11539
+ " workforce <request> same Agent Workforce Ontology route (explicit name)",
11540
+ " legacy-network <request> compatibility-only Hephaestus network route",
10199
11541
  " call \"a,b\" \"<ctx>\" invoke named Hub/Cloud agents (hep-call)",
10200
11542
  " browser [<sub>] real browser execution hardpoint (hep-browser)",
10201
11543
  " route \"<request>\" routing preview — which agent/pipeline would take this",
@@ -10258,7 +11600,7 @@ async function main() {
10258
11600
  runtimeOverride = argv[++i];
10259
11601
  } else if (argv[i] === "--permission" || argv[i] === "-P") {
10260
11602
  const p = (argv[++i] || "").toLowerCase();
10261
- if (!["read", "write", "full"].includes(p)) fail(`알 없는 권한: ${p} (read|write|full)`);
11603
+ if (!["read", "write", "full"].includes(p)) fail(`Unknown permission: ${p} (read|write|full)`);
10262
11604
  PERMISSION = p;
10263
11605
  PERMISSION_EXPLICIT = true;
10264
11606
  } else {
@@ -10319,10 +11661,16 @@ async function main() {
10319
11661
  return cmdCloud(db, rest.slice(1), runtimeOverride);
10320
11662
  case "creds":
10321
11663
  return cmdCreds(db, rest.slice(1));
10322
- case "storm":
10323
- return parity().cmdStorm(db, rest.slice(1), runtimeOverride);
10324
- case "swarm":
10325
- return parity().cmdSwarm(db, rest.slice(1), runtimeOverride);
11664
+ case "storm": {
11665
+ const cwd = projectCwd();
11666
+ const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-storm");
11667
+ return parity().cmdStorm(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
11668
+ }
11669
+ case "swarm": {
11670
+ const cwd = projectCwd();
11671
+ const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-swarm");
11672
+ return parity().cmdSwarm(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
11673
+ }
10326
11674
  case "automation":
10327
11675
  case "automations":
10328
11676
  return parity().cmdAutomation(db, rest.slice(1), runtimeOverride);
@@ -10333,6 +11681,7 @@ async function main() {
10333
11681
  case "build":
10334
11682
  // Terminal-owned preflight: trusted system-global MCP metadata first, one consent,
10335
11683
  // then pass only approved catalog IDs/value-free shortages to the existing builder.
11684
+ ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-build");
10336
11685
  return terminalAssets.cmdBuild({
10337
11686
  db,
10338
11687
  args: rest.slice(1),
@@ -10341,7 +11690,8 @@ async function main() {
10341
11690
  input: process.stdin,
10342
11691
  promptOutput: process.stderr,
10343
11692
  out,
10344
- invokeBuild: (request) => parity().cmdHep(db, request ? ["hep-build", request] : ["hep-build"]),
11693
+ probeMcpServer: (server, probeOptions) => probeApprovedTerminalMcp(db, server, runtimeOverride, projectCwd(), probeOptions),
11694
+ invokeBuild: (request, metadata) => runTerminalBuilder(db, request, metadata, runtimeOverride, projectCwd()),
10345
11695
  });
10346
11696
  case "experience":
10347
11697
  return terminalExperienceExchange.cmdExperienceExchange({
@@ -10363,13 +11713,20 @@ async function main() {
10363
11713
  out,
10364
11714
  });
10365
11715
  case "search": // hep-search — 에이전트 디렉터리 발견 (Hub + 로컬)
10366
- if (!rest[1]) return fail('usage: agentlas search "<찾는 일>" [--limit 10]');
11716
+ if (!rest[1]) return fail('usage: agentlas search "<what you need>" [--limit 10]');
10367
11717
  return parity().cloudSearch(db, rest.slice(1));
10368
11718
  case "install": // public Hub package install — slug로 에이전트 설치
10369
- if (!rest[1]) return fail('usage: agentlas install <slug> (먼저 agentlas search " " 로 찾으세요)');
11719
+ if (!rest[1]) return fail('usage: agentlas install <slug> (run agentlas search "what you need" first)');
10370
11720
  return cmdCloudInstall(db, rest[1]);
11721
+ case "plugin": // Hub 플러그인(MCP 서버 번들) — 에이전트 설치(install)와 다른 카탈로그다
11722
+ case "plugins": {
11723
+ const action = rest[1];
11724
+ if (action === "add") return cmdPluginAdd(db, rest[2]);
11725
+ if (action === "list" || !action) return cmdPluginList();
11726
+ return fail("usage: agentlas plugin add <slug> | agentlas plugin list");
11727
+ }
10371
11728
  case "upload": { // 기본은 owner-private Agent Cloud, public Hub는 명시 flag로만.
10372
- if (!rest[1]) return fail("usage: agentlas upload <에이전트 폴더 경로> [--visibility marketplace]");
11729
+ if (!rest[1]) return fail("usage: agentlas upload <agent-folder-path> [--visibility marketplace]");
10373
11730
  const uploadArgs = rest.slice(1);
10374
11731
  return cmdCloud(db, [cloudActionForTopLevelUpload(uploadArgs), ...uploadArgs], runtimeOverride);
10375
11732
  }
@@ -10379,8 +11736,14 @@ async function main() {
10379
11736
  return parity().cmdHep(db, ["hep-browser", ...rest.slice(1)]);
10380
11737
  case "call": // hep-call — 지정 에이전트 호출/준비
10381
11738
  return parity().cmdHep(db, ["hep-call", ...rest.slice(1)]);
10382
- case "network": // hep-network — A2A 태스크포스 분해/스케줄
10383
- case "taskforce":
11739
+ case "workforce":
11740
+ case "network":
11741
+ case "taskforce": {
11742
+ const cwd = projectCwd();
11743
+ const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-workforce");
11744
+ return workforce().cmdWorkforce(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
11745
+ }
11746
+ case "legacy-network": // explicit compatibility escape hatch only
10384
11747
  return parity().cmdHep(db, ["hep-network", ...rest.slice(1)]);
10385
11748
  case "route": // 라우팅 미리보기 (실행 없음)
10386
11749
  return parity().cmdHep(
@@ -10429,7 +11792,7 @@ async function main() {
10429
11792
  if (firm) return cmdFirm(db, cmd, "", runtimeOverride);
10430
11793
  const prompt = rest.join(" ").trim();
10431
11794
  if (prompt) return cmdAutoRun(db, prompt, runtimeOverride);
10432
- fail(`에이전트/회사를 찾을 없습니다: ${cmd} (agentlas list 로 확인)`);
11795
+ fail(`Agent/company not found: ${cmd} (check with agentlas list)`);
10433
11796
  }
10434
11797
  }
10435
11798
  }
@@ -10463,6 +11826,7 @@ module.exports = {
10463
11826
  replaceMacAppBundle,
10464
11827
  captureRuntime,
10465
11828
  buildArgs,
11829
+ codexCaptureAgentText,
10466
11830
  captureOutputLimit,
10467
11831
  materializeCloudListingCli,
10468
11832
  recoverCloudInstallJournalCli,
@@ -10483,6 +11847,27 @@ module.exports = {
10483
11847
  cloudPortablePathConflict,
10484
11848
  cloudPortableExecutableForFile,
10485
11849
  parseRunExperienceArgs,
11850
+ resolveRuntimeExperienceCli,
11851
+ runTerminalBuilder,
11852
+ resolveRuntime,
11853
+ listAvailableRuntimes,
11854
+ probeApprovedTerminalMcp,
11855
+ finalizeExperienceExecutionCli,
11856
+ buildChildEnvCli,
11857
+ augmentSystem,
11858
+ beginMemoryTurnCli,
11859
+ completeMemoryTurnCli,
11860
+ curatorRuntimeEnvCli,
11861
+ ensureGeminiNoToolsPolicyCli,
11862
+ curateCliReply,
11863
+ TERMINAL_MEMORY_CORE,
11864
+ TERMINAL_MEMORY_CORE_MAX_TOKENS,
11865
+ approximatePromptTokens,
11866
+ memoryEmitterPromptFor,
11867
+ credentialIndexReminderFor,
11868
+ ensureCoreProjectCli,
11869
+ ensureTerminalProjectForExecutionCli,
11870
+ ensureAgentlasProjectStateIgnoreCli,
10486
11871
  DEFAULT_API_MODEL,
10487
11872
  ANTHROPIC_COMPAT_API,
10488
11873
  // 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.