agentlas 0.5.5 → 0.7.0

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.
@@ -29,6 +29,8 @@ const fs = require("node:fs");
29
29
  const { spawn } = require("node:child_process");
30
30
  const crypto = require("node:crypto");
31
31
  const { compareSemVer, normalizeSemVer, parseSemVer } = require("./semver.cjs");
32
+ const terminalAssets = require("./agentlas-experience-mcp.cjs");
33
+ const terminalExperienceExchange = require("./agentlas-experience-exchange.cjs");
32
34
 
33
35
  // ── 앱과 동일한 userData 경로 (electron app.getPath('userData')와 일치) ──
34
36
  function userDataDir() {
@@ -93,6 +95,19 @@ function openNodeSqliteDb(p) {
93
95
  run: (...args) => stmt.run(...args),
94
96
  };
95
97
  },
98
+ // better-sqlite3 API 패리티 — 폴백 경로에서도 db.exec/db.pragma가 있어야 한다.
99
+ // 누락 시 ensureMemoryContextColumn 등 ALTER TABLE(exec)이 TypeError로 조용히 죽어
100
+ // (try/catch 삼킴) context_json 컬럼 마이그레이션이 되지 않고 memory 조회가 깨진다.
101
+ exec: (sql) => db.exec(sql),
102
+ pragma: (source) => {
103
+ const rows = db.prepare(`PRAGMA ${source}`).all();
104
+ // better-sqlite3 pragma()의 단일값 반환 관례를 근사(단일 컬럼·단일 행 → 스칼라).
105
+ if (rows.length === 1) {
106
+ const keys = Object.keys(rows[0]);
107
+ if (keys.length === 1) return rows[0][keys[0]];
108
+ }
109
+ return rows;
110
+ },
96
111
  transaction(fn) {
97
112
  return (...args) => {
98
113
  db.exec("BEGIN");
@@ -280,7 +295,17 @@ const AGENT_BUILD_TERMS = [
280
295
  const BUILD_ENTITY_RE = /(에이전트|agent|팀|team|회사|company)/i;
281
296
  const BUILD_VERB_RE = /(만들|만든|생성|구축|구성해|꾸려|세팅|패키징|scaffold|build|create|\bmake\b|set\s?up|spin\s?up)/i;
282
297
  function isAgentBuildIntent(prompt) {
283
- const p = routeNormalize(prompt);
298
+ // 경로/파일 참조는 빌드 의도의 증거가 아니다 — "/Users/x/agent-tools/notes.md 요약본
299
+ // 만들어줘"의 디렉터리명이나 "agent-notes.md" 같은 파일명이 BUILD_ENTITY_RE를 때려
300
+ // 메타빌더(score 1000)로 직행하던 우회로 차단. 빌드 의도는 산문에서만 읽는다.
301
+ // ⚠️ 남은 슬래시는 통째로 지우지 않고 공백으로만 벌린다 — "에이전트/팀 만들어줘"의
302
+ // 슬래시-엔티티("에이전트/팀")를 삭제하면 BUILD_ENTITY_RE가 못 맞아 빌드 의도를 놓친다.
303
+ // 진짜 경로는 이미 routeStripPaths(마지막 세그먼트만)+확장자 제거가 처리했다.
304
+ const p = routeNormalize(
305
+ routeStripPaths(prompt)
306
+ .replace(/\S+\.[A-Za-z0-9]{1,6}(?=\s|$)/g, " ")
307
+ .replace(/[\\/]+/g, " "),
308
+ );
284
309
  if (!p.trim() || isTrivialRoutePrompt(p)) return false;
285
310
  if (AGENT_BUILD_TERMS.some((term) => p.includes(routeNormalize(term)))) return true;
286
311
  // 예: "단일 에이전트 하나만 만들어줘", "팀 좀 꾸려줘", "make me an agent"
@@ -300,7 +325,11 @@ function resolveMetaBuilder(db) {
300
325
  }
301
326
  return null;
302
327
  }
303
- const ROUTE_STOP_WORDS = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "make", "build", "create", "agent", "agents", "please", "좀", "해주세요", "해줘", "만들어", "붙여", "연결", "작업", "요청"]);
328
+ // "ai"/"llm" 같은 초범용 토큰은 모든 에이전트 프롬프트에 나오므로 판별력이 0이다
329
+ // 이런 단어 하나로 전문 에이전트가 선택되던 오라우팅(예: 일반 맥 질문 → Pitch Deck Architect)을 막는다.
330
+ // "local"/"imported"/"team"은 임포터 보일러플레이트("Imported local team")와 slug 접두/접미에
331
+ // 편재해 판별력이 없다 — 'team' 한 단어가 아무 임포트 팀의 slug 부분문자열(+6 strong)을 때리던 구멍.
332
+ const ROUTE_STOP_WORDS = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "make", "build", "create", "agent", "agents", "team", "please", "ai", "llm", "local", "imported", "인공지능", "에이아이", "좀", "해주세요", "해줘", "만들어", "붙여", "연결", "작업", "요청"]);
304
333
  const ROUTE_HINTS = [
305
334
  {
306
335
  slug: "agentlas-app-builder",
@@ -358,20 +387,46 @@ const ROUTE_HINTS = [
358
387
  function routeNormalize(value) {
359
388
  return String(value || "").toLowerCase().replace(/[_/]+/g, "-");
360
389
  }
390
+ // 경로 디렉터리 성분은 라우팅 의도가 아니다 — 마지막 세그먼트(파일/폴더명)만 남긴다.
391
+ // 사고(2026-07-12): "/Users/mason/Documents/…/Appbridge_Template.이 …" 프롬프트의 경로 토큰
392
+ // ("users","mason","documents","users-mason-documents-")이 임포트 에이전트 system_prompt 속
393
+ // 절대경로와 맞아떨어져 +2씩 쌓이고 라우팅 근거에까지 노출됐다. 프롬프트/헤이스택 양쪽에
394
+ // 대칭 적용해 경로↔경로 우연 일치를 차단한다. 파일/폴더명은 실제 의도라서 보존한다.
395
+ // 규칙: 공백/인용부호/괄호 뒤(또는 문자열 시작)에서 시작하고, "세그먼트+구분자"가 2회 이상
396
+ // 이어지는 절대·홈·드라이브·UNC 경로만 경로로 본다 — "and/or", "서울/부산", 날짜(2026/07/12),
397
+ // "https://…"(콜론 뒤 //는 시작 조건 불충족)는 건드리지 않는다. 세그먼트 안의 단일 공백은
398
+ // 뒤가 대문자로 시작할 때만 허용해 "Mobile Documents"/"Application Support"는 접되,
399
+ // "/tmp/out 기획/디자인 …" 같은 한글 프로즈를 경로로 삼켜버리지 않는다. 상대경로는
400
+ // 확장자 있는 파일 참조("docs/plan/roadmap.md")만 접는다 — 디렉터리명("plan")이 힌트/이름
401
+ // strong 채널을 때리는 것을 막으면서 "서울/부산/대구" 같은 나열은 보존한다.
402
+ const ROUTE_PATH_RE = /(^|[\s"'`(<\[{])((?:~|[A-Za-z]:)?[\\/]{1,2}(?:[^\s\\/]+(?: [A-Z][^\s\\/]*)?[\\/]+){2,}[^\s\\/]*|(?:[^\s\\/]+[\\/]+){2,}[^\s\\/]+\.[A-Za-z0-9]{1,6})/g;
403
+ function routeStripPaths(value) {
404
+ return String(value || "").replace(ROUTE_PATH_RE, (whole, pre, p) => {
405
+ const segs = p.split(/[\\/]+/).filter(Boolean);
406
+ return pre + (segs.length ? segs[segs.length - 1] : "");
407
+ });
408
+ }
361
409
  function routeTokenize(value) {
362
- const matches = routeNormalize(value).match(/[a-z0-9][a-z0-9-]{1,}|[가-힣]{2,}/g) || [];
410
+ // 매치가 영숫자로 끝나도록 강제해 "users-mason-documents-" 같은 후행 하이픈 토큰을 원천 차단.
411
+ const matches = routeNormalize(routeStripPaths(value)).match(/[a-z0-9][a-z0-9-]*[a-z0-9]|[가-힣]{2,}/g) || [];
363
412
  const expanded = matches.flatMap((term) => term.split("-").filter(Boolean).concat(term));
364
413
  return [...new Set(expanded.filter((term) => term.length >= 2 && !ROUTE_STOP_WORDS.has(term)))];
365
414
  }
415
+ // 정체성 존(slug/이름/태그라인) — 여기 적중은 강한 라우팅 신호. system_prompt 본문 적중은 약한 신호.
416
+ // 임포터 보일러플레이트 태그라인("Imported local team/agent")의 세 단어는 전부 스톱워드라
417
+ // 프롬프트 토큰이 될 수 없다 — 별도 필터 불필요.
418
+ function routeIdentityHaystack(agent) {
419
+ return routeNormalize(routeStripPaths([agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].join("\n")));
420
+ }
366
421
  function routeHaystack(agent) {
367
- return routeNormalize([
422
+ return routeNormalize(routeStripPaths([
368
423
  agent.slug,
369
424
  agent.name,
370
425
  agent.name_en,
371
426
  agent.tagline,
372
427
  agent.tagline_en,
373
428
  String(agent.system_prompt || "").slice(0, 3500),
374
- ].join("\n"));
429
+ ].join("\n")));
375
430
  }
376
431
  const APP_BUILDER_EXPLICIT_TERMS = [
377
432
  "apps generate", "app builder", "make an app", "build an app", "create an app",
@@ -414,7 +469,7 @@ function isTrivialRoutePrompt(promptText) {
414
469
  return words.length <= 3 && TRIVIAL_ROUTE_PROMPTS.has(stripped);
415
470
  }
416
471
  function isAppBuilderWorthyRoutePrompt(prompt) {
417
- const promptText = routeNormalize(prompt);
472
+ const promptText = routeNormalize(routeStripPaths(prompt));
418
473
  if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return false;
419
474
  const explicit = routeMatchedTerms(promptText, APP_BUILDER_EXPLICIT_TERMS);
420
475
  if (explicit.length) return true;
@@ -436,8 +491,10 @@ function routeHint(promptText, agent, lang) {
436
491
  if (!terms.length) return { score: 0, terms: [], reason: "" };
437
492
  return { score: 12 + terms.length * 3, terms, reason: lang === "ko" ? hint.reasonKo : hint.reasonEn };
438
493
  }
439
- function scoreRouteAgent(prompt, promptTerms, agent, lang) {
440
- const promptText = routeNormalize(prompt);
494
+ function scoreRouteAgent(prompt, promptTerms, agent, lang, pre) {
495
+ // 대칭 스트리핑 필수: promptText 이름(+20)·힌트(+12↑) strong 채널의 입력이라, 여기서
496
+ // 경로를 안 벗기면 "/Users/x/project-plan/…"의 디렉터리명이 strong 게이트를 그대로 뚫는다.
497
+ const promptText = routeNormalize(routeStripPaths(prompt));
441
498
  if (agent.slug === "agentlas-app-builder" && !isAppBuilderWorthyRoutePrompt(promptText)) {
442
499
  return {
443
500
  agent,
@@ -446,27 +503,41 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
446
503
  ? "전용 App을 만들 만큼 반복·상태·편집·자동화가 뚜렷하지 않아 App Builder 라우트를 보류했습니다"
447
504
  : "the request does not clearly need a dedicated App with durable workflow, state, editing, or automation",
448
505
  terms: [],
506
+ strong: false,
449
507
  };
450
508
  }
451
- const haystack = routeHaystack(agent);
509
+ // 헤이스택은 설치/임포트 시에만 변하므로 autoRouteAgent가 미리 계산해 넘긴다(중복 계산 제거).
510
+ const identityHay = (pre && pre.identityHay) || routeIdentityHaystack(agent);
511
+ const haystack = (pre && pre.haystack) || routeHaystack(agent);
452
512
  let score = 0;
513
+ let strong = false; // 이름 언급/정체성 적중/큐레이션 힌트 — 데스크탑처럼 "이름/힌트급 증거"가 있어야 위임한다
453
514
  const terms = [];
515
+ const seenNames = new Set();
454
516
  for (const name of [agent.slug, agent.name, agent.name_en].filter(Boolean)) {
455
517
  const n = routeNormalize(name);
456
518
  // 4자 미만 일반 단어("team","agent" 등)가 프롬프트에 우연히 들어가 +20을 독식하지 않도록 가드.
457
- if (n && n.length >= 4 && promptText.includes(n)) {
519
+ // name === name_en 인 임포트 에이전트(appbridge 등)가 +20을 받지 않도록 정규화 기준 dedupe.
520
+ if (!n || n.length < 4 || seenNames.has(n)) continue;
521
+ seenNames.add(n);
522
+ if (promptText.includes(n)) {
458
523
  score += 20;
459
524
  terms.push(name);
525
+ strong = true;
460
526
  }
461
527
  }
462
528
  for (const term of promptTerms) {
463
- if (haystack.includes(term)) {
529
+ if (identityHay.includes(term)) {
530
+ score += 6; // 이름/태그라인 적중 = 그 에이전트의 정체성 자체를 부른 것
531
+ terms.push(term);
532
+ strong = true;
533
+ } else if (haystack.includes(term)) {
464
534
  score += term.length >= 5 ? 3 : 2;
465
535
  terms.push(term);
466
536
  }
467
537
  }
468
538
  const hint = routeHint(promptText, agent, lang);
469
539
  score += hint.score;
540
+ if (hint.score) strong = true;
470
541
  terms.push(...hint.terms);
471
542
  const unique = [...new Set(terms)].slice(0, 6);
472
543
  const reason = hint.reason || (lang === "ko"
@@ -476,7 +547,33 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
476
547
  : unique.length
477
548
  ? `request terms ${unique.map((term) => `"${term}"`).join(", ")} best match this agent's role/triggers`
478
549
  : "no specialist matched clearly, so the default project coordinator is safest");
479
- return { agent, score, reason, terms: unique };
550
+ return { agent, score, reason, terms: unique, strong };
551
+ }
552
+ // 라우팅 확신 임계값 — 데스크탑 auto-router의 MIN_SPECIALIST_SCORE(10)와 동일 기준.
553
+ // 위임에는 점수뿐 아니라 strong 신호(이름 포함 +20 / 정체성 적중 +6 / 큐레이션 힌트 +12↑)가
554
+ // 반드시 있어야 한다. system_prompt 본문의 약한 단어 적중(+2~3)이 몇 개 쌓여도, strong 신호가
555
+ // 없으면 절대 위임하지 않는다. 미달이면 "직답"(에이전트·능력 라우팅 없음) — 일반 질문이
556
+ // Pitch Deck Architect 같은 무관 페르소나 + gemini 이미지 런타임으로 끌려가던 사고의 근본 수리.
557
+ const MIN_ROUTE_SCORE = 10;
558
+ function directRouteChoice(lang) {
559
+ const resolvedLang = lang || prefsLang();
560
+ return {
561
+ direct: true,
562
+ agent: null,
563
+ score: 0,
564
+ terms: [],
565
+ strong: false,
566
+ reason: resolvedLang === "ko"
567
+ ? "특정 전문 에이전트가 필요 없는 일반 요청입니다"
568
+ : "this is a general request that needs no specialist agent",
569
+ };
570
+ }
571
+ // 직답 모드 시스템 프롬프트 — 페르소나·라우팅 오염 없이 현재 런타임 그대로 답한다.
572
+ function directSystemPrompt(lang) {
573
+ const resolvedLang = lang || prefsLang();
574
+ return resolvedLang === "ko"
575
+ ? "당신은 Agentlas 터미널의 기본 어시스턴트입니다. 특별한 페르소나 없이 사용자의 요청에 정확하고 간결하게 바로 답하세요. 에이전트 라우팅이나 이미지 생성 능력을 스스로 언급하지 마세요."
576
+ : "You are the Agentlas terminal's default assistant. Answer the user's request directly and concisely, with no special persona. Do not bring up agent routing or image-generation capabilities on your own.";
480
577
  }
481
578
  function autoRouteAgent(db, prompt, lang) {
482
579
  const resolvedLang = lang || prefsLang();
@@ -487,6 +584,7 @@ function autoRouteAgent(db, prompt, lang) {
487
584
  return {
488
585
  agent: meta,
489
586
  score: 1000,
587
+ strong: true,
490
588
  reason:
491
589
  resolvedLang === "ko"
492
590
  ? "새 에이전트/팀/회사를 만드는 요청이라 메타에이전트(빌더)로 라우팅했습니다"
@@ -496,28 +594,46 @@ function autoRouteAgent(db, prompt, lang) {
496
594
  }
497
595
  }
498
596
  const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
499
- if (!agents.length) return null;
500
- const terms = routeTokenize(prompt);
501
- const ranked = agents.map((agent) => scoreRouteAgent(prompt, terms, agent, resolvedLang)).sort((a, b) => b.score - a.score);
502
- if (ranked[0] && ranked[0].score > 0) return ranked[0];
503
- const fallback = agents.find((agent) => agent.slug === "agentlas-pm-soul") || agents[0];
504
- return {
505
- agent: fallback,
506
- score: 0,
507
- reason: resolvedLang === "ko"
508
- ? "명확한 전문 에이전트가 없어 기본 프로젝트 조율 경로를 선택했습니다"
509
- : "no specialist matched clearly, so Agentlas chose the default coordination route",
510
- terms: [],
511
- };
597
+ if (!agents.length) return directRouteChoice(resolvedLang);
598
+ let terms = routeTokenize(prompt);
599
+ // 헤이스택은 번만 계산해 IDF와 스코어링 양쪽에서 재사용한다.
600
+ const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
601
+ // IDF 근사 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등) 판별력이 없어 제외.
602
+ if (agents.length >= 3) {
603
+ terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
604
+ }
605
+ const ranked = agents
606
+ .map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
607
+ .sort((a, b) => b.score - a.score);
608
+ // 1위가 아니라 "임계값+strong을 모두 만족하는 최고 순위"를 뽑는다 — 장황한 프롬프트의
609
+ // 약한 단어 적중이 점수 1위를 먹어도, 자격 있는 전문 에이전트가 직답으로 밀려나지 않는다.
610
+ const pick = ranked.find((r) => r.score >= MIN_ROUTE_SCORE && r.strong);
611
+ if (pick) return pick;
612
+ return directRouteChoice(resolvedLang);
512
613
  }
513
614
  function autoRouteNote(choice, lang) {
514
- const name = (lang || prefsLang()) === "ko" ? choice.agent.name : choice.agent.name_en || choice.agent.name;
515
- return (lang || prefsLang()) === "ko"
615
+ const resolvedLang = lang || prefsLang();
616
+ if (choice.direct) {
617
+ return resolvedLang === "ko"
618
+ ? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}.`
619
+ : `Selected agent: none — answering directly. Reason: ${choice.reason}.`;
620
+ }
621
+ const name = resolvedLang === "ko" ? choice.agent.name : choice.agent.name_en || choice.agent.name;
622
+ return resolvedLang === "ko"
516
623
  ? `사용 에이전트: ${name}. 이유: ${choice.reason}.`
517
624
  : `Selected agent: ${name}. Reason: ${choice.reason}.`;
518
625
  }
519
626
  function autoRoutePreamble(choice, lang) {
520
627
  const resolvedLang = lang || prefsLang();
628
+ if (choice.direct) {
629
+ return [
630
+ "## Agentlas direct answer",
631
+ "",
632
+ resolvedLang === "ko"
633
+ ? "이 요청은 전문 에이전트 라우팅 없이 처리합니다. 라우팅이나 에이전트를 언급하지 말고 사용자 요청에 바로 답하세요."
634
+ : "This request is handled without specialist routing. Answer the user directly, without mentioning routing or agents.",
635
+ ].join("\n");
636
+ }
521
637
  const appBuilderNeedsConsent = choice.agent && choice.agent.slug === "agentlas-app-builder";
522
638
  const instruction = appBuilderNeedsConsent
523
639
  ? resolvedLang === "ko"
@@ -708,9 +824,14 @@ function importLocalFolderCli(db, absPath) {
708
824
  ).run(id, slug, name, name, tagline, tagline, systemPrompt, envReqsJson, now, tone);
709
825
  }
710
826
  }
827
+ // detectKind 결과를 DB에도 기록 — needsImage의 팀 body-veto 등 능력 판정이
828
+ // 데스크탑이 써준 entity_kind에 무임승차하지 않고 터미널 단독 임포트에서도 성립한다.
829
+ if (columnExists(db, "installed_agents", "entity_kind")) {
830
+ db.prepare("UPDATE installed_agents SET entity_kind=? WHERE id=?").run(kind, id);
831
+ }
711
832
  // 라우트 저장
712
833
  routes[id] = { agentId: id, path: dir, runtime, labels, kind, importedAt: now };
713
- fs.writeFileSync(path.join(userDataDir(), "agent-routes.json"), JSON.stringify(routes, null, 2), "utf8");
834
+ writeJsonPrivateAtomicCli(path.join(userDataDir(), "agent-routes.json"), routes);
714
835
 
715
836
  // 팀이면 회사(firm)로도 등록 → 앱 FIRMS 목록 + `agentlas firm <slug>` 사용 가능. slug 기준 멱등.
716
837
  let firm = null;
@@ -785,6 +906,7 @@ const CLOUD_AGENT_FILES = new Set(["AGENT.md", "AGENTS.md", "CLAUDE.md", "GEMINI
785
906
  const CLOUD_SKIP_DIRS = new Set([".git", ".next", ".studio-runtime", ".turbo", "build", "coverage", "dist", "node_modules", "out", "release"]);
786
907
  const CLOUD_BLOCKED_FILE_RE = [/^\.env(?:\..*)?$/i, /^id_rsa(?:\.pub)?$/i, /^credentials(?:\..*)?$/i, /^secrets?(?:\..*)?$/i, /^cloud-asset-state\.v1\.json$/i, /(?:^|[._-])service-account(?:[._-]|$)/i, /\.(?:key|pem|p12|pfx|mobileprovision)$/i];
787
908
  const CLOUD_ROUTING_CARD_PATH = ".agentlas/routing-card.json";
909
+ const CLOUD_LOCAL_EXPERIENCE_LINEAGE_PATH = ".agentlas/experience-relations.jsonl";
788
910
  const CLOUD_ROUTING_CARD_CAPABILITY_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
789
911
  const CLOUD_ROUTING_CARD_STATUSES = new Set(["draft", "searchable", "candidate", "routing_ready", "trusted"]);
790
912
  const CLOUD_SECRET_RE = [
@@ -1574,6 +1696,12 @@ function scanCloudFolderCli(rootPath) {
1574
1696
  if (entry.name.startsWith("._")) continue;
1575
1697
  const abs = path.join(dir, entry.name);
1576
1698
  const rel = path.relative(rootPath, abs).split(path.sep).join("/");
1699
+ if (cloudIsLocalExperienceLineagePath(rel)) {
1700
+ let bytes = 0;
1701
+ try { bytes = Number(fs.lstatSync(abs).size) || 0; } catch { /* excluded local state */ }
1702
+ files.push({ path: rel, bytes, sha256: "", kind: "text", included: false, reason: "experience-lineage-separate-asset" });
1703
+ continue;
1704
+ }
1577
1705
  if (cloudPortablePathKey(rel) === cloudPortablePathKey(CLOUD_RESTORE_MARKER_PATH)) {
1578
1706
  // Local restore/CAS metadata is runtime state, never portable asset
1579
1707
  // data, but it must be captured with the same no-follow stability gate.
@@ -2318,6 +2446,17 @@ function persistCloudListingCli(db, listing) {
2318
2446
  throw error;
2319
2447
  }
2320
2448
  const localPath = restore?.path || null;
2449
+ // entity_kind 기록 — needsImage의 팀 body-veto가 로컬 폴더 임포트(detectKind)뿐 아니라
2450
+ // 클라우드/Hub 소스 설치 팀에도 걸리게 한다. 안 하면 팀 CEO 두뇌의 부서 키워드
2451
+ // ("Design HQ" 등)로 needsImage가 참이 되어 세션 런타임이 통째로 gemini로 하이재킹된다.
2452
+ // Hub가 준 entityKind를 우선하고, 없으면 materialize된 팩 폴더 구조로 판정한다.
2453
+ if (columnExists(db, "installed_agents", "entity_kind")) {
2454
+ let kind = String(listing.entityKind || "").toLowerCase();
2455
+ if (kind !== "team" && kind !== "agent") {
2456
+ kind = localPath && fs.existsSync(localPath) ? detectKind(localPath) : "agent";
2457
+ }
2458
+ db.prepare("UPDATE installed_agents SET entity_kind=? WHERE id=?").run(kind, id);
2459
+ }
2321
2460
  return existing
2322
2461
  ? { ...existing, slug, name: dbExpected.name, ...(localPath ? { localPath } : {}) }
2323
2462
  : { id, slug, name: dbExpected.name, ...(localPath ? { localPath } : {}) };
@@ -2663,6 +2802,13 @@ function printCloudPackageResult(result) {
2663
2802
  function cloudPackageSnapshot(files) {
2664
2803
  return new Map(files.map((file) => [file.path, file]));
2665
2804
  }
2805
+ function cloudIsLocalExperienceLineagePath(value) {
2806
+ const normalized = cloudPortablePathKey(String(value || "").replace(/\\/g, "/"));
2807
+ const canonical = cloudPortablePathKey(CLOUD_LOCAL_EXPERIENCE_LINEAGE_PATH);
2808
+ return normalized === canonical
2809
+ || normalized.startsWith(`${canonical}.`)
2810
+ || normalized.startsWith(cloudPortablePathKey(".agentlas/.experience-relations.jsonl."));
2811
+ }
2666
2812
  function cloudReadPublicCareerCard(snapshot, findings) {
2667
2813
  const relativePath = ".agentlas/public-career-card.json";
2668
2814
  const file = snapshot.get(relativePath);
@@ -2855,7 +3001,7 @@ function cloudHashPackage(files, version = CLOUD_PACKAGE_HASH_V1) {
2855
3001
  // 서버 package-contract.ts와 바이트 동일해야 한다: 경로 코드포인트 순 정렬.
2856
3002
  // 정렬 없이 스캔 순서로 해시하면 대소문자 혼합 경로 패키지(AGENTS.md + agents/…)가
2857
3003
  // 전부 package_hash_mismatch로 거절된다(2026-07-02 근본 수정).
2858
- for (const file of [...files].sort(cloudCodePointPathOrder)) {
3004
+ for (const file of [...files].filter((file) => !cloudIsLocalExperienceLineagePath(file.path)).sort(cloudCodePointPathOrder)) {
2859
3005
  h.update(file.path);
2860
3006
  h.update("\0");
2861
3007
  h.update(file.sha256);
@@ -6940,6 +7086,21 @@ function readJsonSafeCli(filePath, fallback) {
6940
7086
  function writeJsonSafeCli(filePath, value) {
6941
7087
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
6942
7088
  }
7089
+ // 원자적(temp+rename) + 소유자 전용(0600) JSON 쓰기. 세션 ID/경로 등 민감 상태 파일용:
7090
+ // (1) 크래시 중간 쓰기로 JSON이 깨져 routesMap()이 {}를 돌려주며 임포트 매핑을 통째로 잃던 사고,
7091
+ // (2) 기본 umask(0644)로 cli-sessions.json/agent-routes.json이 world-readable이던 정보 노출을 함께 막는다.
7092
+ function writeJsonPrivateAtomicCli(filePath, value) {
7093
+ const dir = path.dirname(filePath);
7094
+ const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
7095
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
7096
+ try {
7097
+ fs.renameSync(tmp, filePath);
7098
+ } catch (e) {
7099
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
7100
+ throw e;
7101
+ }
7102
+ try { fs.chmodSync(filePath, 0o600); } catch { /* 일부 FS는 chmod 미지원 — best-effort */ }
7103
+ }
6943
7104
 
6944
7105
  function ontologySourceManifestSkeletonCli(root) {
6945
7106
  return {
@@ -7802,10 +7963,30 @@ async function runApi(backend, model, system, prompt, options) {
7802
7963
  async function executeOnce(db, system, prompt, override, ctx) {
7803
7964
  ctx = ctx || { projectPath: null, agentId: null };
7804
7965
  if (!ctx.cwdAtRequest) ctx.cwdAtRequest = projectCwd();
7966
+ let runtimeSystem = system;
7967
+ let localExperienceContext = null;
7968
+ if (ctx.runtimeExperience && ctx.runtimeExperience.disabled !== true) {
7969
+ const runtimeExperience = ctx.runtimeExperience;
7970
+ const augmented = terminalExperienceExchange.augmentRuntimeSystemWithLocalExperience(system, {
7971
+ userDataDir: userDataDir(),
7972
+ cwd: ctx.projectPath || ctx.cwdAtRequest,
7973
+ baseAgentReleaseId: runtimeExperience.baseAgentReleaseId,
7974
+ agentDefinitionId: runtimeExperience.agentDefinitionId,
7975
+ taskSignatures: runtimeExperience.taskSignatures || [],
7976
+ environmentTags: Array.isArray(runtimeExperience.environmentTags) && runtimeExperience.environmentTags.length
7977
+ ? runtimeExperience.environmentTags
7978
+ : terminalExperienceExchange.defaultEnvironmentTags(),
7979
+ });
7980
+ runtimeSystem = augmented.systemPrompt;
7981
+ localExperienceContext = augmented.experienceContext;
7982
+ if (localExperienceContext.itemIds.length) {
7983
+ process.stderr.write(`▸ local Experience advisory · ${localExperienceContext.itemIds.length} item(s) · ~${localExperienceContext.estimatedTokens} tokens · no server rental receipt\n`);
7984
+ }
7985
+ }
7805
7986
  const rt = resolveRuntime(db, override);
7806
7987
  if (rt.mode === "cli") {
7807
7988
  // 네이티브 CLI는 자체 세션을 가지므로 emitter는 넣지 않고(노이즈 방지) 메모리 컨텍스트만 주입.
7808
- const sys = augmentSystem(db, system, ctx, false);
7989
+ const sys = augmentSystem(db, runtimeSystem, ctx, false);
7809
7990
  const cwd = ctx.projectPath || projectCwd();
7810
7991
  const permission = ctx.permission || "write";
7811
7992
  const env = await buildChildEnvCli(db, { ...ctx, cwd });
@@ -7839,7 +8020,7 @@ async function executeOnce(db, system, prompt, override, ctx) {
7839
8020
  return res.error ? 1 : 0;
7840
8021
  }
7841
8022
  // API 경로 — emitter 동봉 → 답변에서 메모리 이벤트를 파싱·큐레이션하고 블록은 제거.
7842
- const sys = augmentSystem(db, system, ctx, true);
8023
+ const sys = augmentSystem(db, runtimeSystem, ctx, true);
7843
8024
  const env = await buildChildEnvCli(db, { ...ctx, cwd: ctx.cwd || projectCwd() });
7844
8025
  Object.assign(process.env, env);
7845
8026
  process.stderr.write(`▸ ${rt.backend}${rt.model ? " · " + rt.model : ""}\n`);
@@ -8009,13 +8190,28 @@ const PROTECTED_CHILD_ENV_KEYS_CLI = new Set([
8009
8190
  "AGENTLAS_NATIVE_IDLE_TIMEOUT_MS", "AGENTLAS_NATIVE_TOTAL_TIMEOUT_MS", "AGENTLAS_NATIVE_KILL_GRACE_MS",
8010
8191
  "AGENTLAS_CAPTURE_MAX_OUTPUT_BYTES",
8011
8192
  ]);
8012
- function isProtectedChildEnvKeyCli(key) {
8013
- return PROTECTED_CHILD_ENV_KEYS_CLI.has(String(key || "").trim().toUpperCase());
8193
+ // 네트워크 무결성 키 — TLS 검증·프록시·CA·엔드포인트·세션. 프로젝트/에이전트 dotenv(신뢰 불가:
8194
+ // 클론한 레포에 딸려올 수 있음)로 주입되면 MITM/SSRF/세션 하이재킹이 된다. 단, 사용자 본인의
8195
+ // 전역 credentials.env와 호스트 셸 env는 신뢰하므로 그대로 허용한다. 사고 방지: 원샷 API 경로는
8196
+ // buildChildEnvCli 결과를 process.env에 병합(Object.assign)하므로, 프로젝트 .env가 부모 프로세스의
8197
+ // 클라우드 호출(세션 쿠키 동반)까지 오염시킬 수 있었다.
8198
+ const UNTRUSTED_PROTECTED_ENV_KEYS_CLI = new Set([
8199
+ "NODE_TLS_REJECT_UNAUTHORIZED", "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
8200
+ "OPENSSL_CONF", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE",
8201
+ "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "GRPC_PROXY", "NPM_CONFIG_PROXY",
8202
+ "AGENTLAS_SESSION", "AGENTLAS_MCP_BASE_URL", "AGENTLAS_WEB_BASE_URL", "AGENTLAS_API_BASE_URL",
8203
+ "AGENTLAS_HUB_BASE_URL", "AGENTLAS_CLOUD_BASE_URL", "OLLAMA_HOST",
8204
+ ]);
8205
+ function isProtectedChildEnvKeyCli(key, trusted) {
8206
+ const k = String(key || "").trim().toUpperCase();
8207
+ if (PROTECTED_CHILD_ENV_KEYS_CLI.has(k)) return true; // 호스트 신원/플러그인 루트 — 모든 출처 차단
8208
+ if (!trusted && UNTRUSTED_PROTECTED_ENV_KEYS_CLI.has(k)) return true; // 네트워크 무결성 — 비신뢰 출처만 차단
8209
+ return false;
8014
8210
  }
8015
- function mergeChildEnvValuesCli(target, values, overwrite) {
8211
+ function mergeChildEnvValuesCli(target, values, overwrite, trusted) {
8016
8212
  const injected = [];
8017
8213
  for (const [key, value] of Object.entries(values || {})) {
8018
- if (!value || isProtectedChildEnvKeyCli(key)) continue;
8214
+ if (!value || isProtectedChildEnvKeyCli(key, trusted)) continue;
8019
8215
  if (!overwrite && target[key]) continue;
8020
8216
  target[key] = value;
8021
8217
  injected.push(key);
@@ -8024,19 +8220,20 @@ function mergeChildEnvValuesCli(target, values, overwrite) {
8024
8220
  }
8025
8221
  async function buildChildEnvCli(db, ctx) {
8026
8222
  const env = { ...process.env };
8027
- const apply = (values, overwrite) => {
8028
- mergeChildEnvValuesCli(env, values, overwrite);
8223
+ // trusted=true: 사용자 본인의 전역 자격/볼트. trusted=false: 프로젝트·에이전트 폴더 dotenv.
8224
+ const apply = (values, overwrite, trusted) => {
8225
+ mergeChildEnvValuesCli(env, values, overwrite, trusted);
8029
8226
  };
8030
8227
  const globalCredentials = {
8031
8228
  ...readDotEnvFileCli(path.join(userDataDir(), "credentials.env")),
8032
8229
  ...readDotEnvFileCli(path.join(os.homedir(), ".agentlas", "credentials.env")),
8033
8230
  };
8034
- apply(globalCredentials, false);
8035
- if (ctx && ctx.projectPath) apply(projectScopedEnvValuesCli(globalCredentials, ctx.projectPath), true);
8036
- if (ctx && ctx.cwd) apply(readDotEnvDirCli(ctx.cwd), true);
8037
- if (ctx && ctx.projectPath) apply(readDotEnvDirCli(ctx.projectPath), true);
8231
+ apply(globalCredentials, false, true);
8232
+ if (ctx && ctx.projectPath) apply(projectScopedEnvValuesCli(globalCredentials, ctx.projectPath), true, true);
8233
+ if (ctx && ctx.cwd) apply(readDotEnvDirCli(ctx.cwd), true, false);
8234
+ if (ctx && ctx.projectPath) apply(readDotEnvDirCli(ctx.projectPath), true, false);
8038
8235
  const agentDir = agentEnvDirCli(ctx && ctx.agentId);
8039
- if (agentDir) apply(readDotEnvDirCli(agentDir), true);
8236
+ if (agentDir) apply(readDotEnvDirCli(agentDir), true, false);
8040
8237
 
8041
8238
  const mm = loadMultimodalCatalog();
8042
8239
  const settings = getMultimodalSettingsCli(db);
@@ -8045,7 +8242,7 @@ async function buildChildEnvCli(db, ctx) {
8045
8242
  if (req && req.key) keys.add(req.key);
8046
8243
  }
8047
8244
  const vaultValues = await readVaultEnvValuesCli([...keys].filter((key) => !env[key]), ctx && ctx.projectPath);
8048
- apply(vaultValues, false);
8245
+ apply(vaultValues, false, true); // 볼트는 사용자 본인 저장소 — 신뢰
8049
8246
  env.AGENTLAS_MULTIMODAL_IMAGE_PROVIDER = settings.imageProvider;
8050
8247
  env.AGENTLAS_MULTIMODAL_VIDEO_PROVIDER = settings.videoProvider;
8051
8248
  env.AGENTLAS_MULTIMODAL_AUDIO_PROVIDER = settings.audioProvider;
@@ -8117,6 +8314,7 @@ function buildHelpers(db) {
8117
8314
  autoRouteAgent: (db_, prompt, lang) => autoRouteAgent(db_, prompt, lang),
8118
8315
  autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
8119
8316
  autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
8317
+ directSystemPrompt: (lang) => directSystemPrompt(lang),
8120
8318
  cliMemoryContext: (db_, pp) => cliMemoryContext(db_, pp),
8121
8319
  importLocal: (db_, p) => importLocalFolderCli(db_, p),
8122
8320
  // REPL-safe public Hub install: fail()(process.exit) 대신 Error를 throw 해 REPL이 직접 렌더하게 한다.
@@ -8164,11 +8362,21 @@ function buildHelpers(db) {
8164
8362
  try { return JSON.parse(fs.readFileSync(path.join(userDataDir(), "cli-sessions.json"), "utf8")) || []; } catch { return []; }
8165
8363
  },
8166
8364
  sessionsSave: (list) => {
8167
- try { fs.writeFileSync(path.join(userDataDir(), "cli-sessions.json"), JSON.stringify((list || []).slice(0, 30), null, 2), "utf8"); } catch { /* ignore */ }
8365
+ try { writeJsonPrivateAtomicCli(path.join(userDataDir(), "cli-sessions.json"), (list || []).slice(0, 30)); } catch { /* ignore */ }
8168
8366
  },
8169
8367
  // 패리티: REPL의 /storm·/swarm·/build·/route·/research 가 그대로 호출한다.
8170
8368
  stormRun: (db_, goal, ctx) => parity().stormRun(db_, goal, ctx),
8171
8369
  swarmRun: (db_, goal, ctx) => parity().swarmRun(db_, goal, ctx),
8370
+ terminalBuild: (db_, args, ctx = {}) => terminalAssets.cmdBuild({
8371
+ db: db_,
8372
+ args: Array.isArray(args) ? args : terminalAssets.tokenizeBuildCommandLine(String(args || "")),
8373
+ userDataDir: userDataDir(),
8374
+ cwd: ctx.cwd || projectCwd(),
8375
+ input: ctx.input || process.stdin,
8376
+ promptOutput: ctx.promptOutput || process.stderr,
8377
+ out: ctx.out || out,
8378
+ invokeBuild: (request) => parity().cmdHep(db_, request ? ["hep-build", request] : ["hep-build"]),
8379
+ }),
8172
8380
  hepRun: (args, opts) => parity().runHephaestusInteractive(args, opts),
8173
8381
  cloudSearch: (db_, args) => parity().cloudSearch(db_, args),
8174
8382
  careerGraphCommand: (text, ctx) => runCareerGraphNaturalCli(text, {
@@ -8517,25 +8725,68 @@ function cmdCd(db, query) {
8517
8725
  process.stdout.write(folder + "\n");
8518
8726
  }
8519
8727
 
8520
- async function cmdRun(db, query, prompt, runtimeOverride) {
8728
+ function parseRunExperienceArgs(args) {
8729
+ const prompt = [];
8730
+ const experience = { taskSignatures: [], environmentTags: [] };
8731
+ let passthrough = false;
8732
+ const addList = (target, value) => {
8733
+ for (const item of String(value || "").split(",").map((entry) => entry.trim()).filter(Boolean)) {
8734
+ if (!target.includes(item)) target.push(item);
8735
+ }
8736
+ };
8737
+ for (let index = 0; index < args.length; index += 1) {
8738
+ const token = String(args[index]);
8739
+ if (passthrough) { prompt.push(token); continue; }
8740
+ if (token === "--") { passthrough = true; continue; }
8741
+ const take = () => index + 1 < args.length ? String(args[++index]) : "";
8742
+ if (token === "--experience-base-release") experience.baseAgentReleaseId = take();
8743
+ else if (token.startsWith("--experience-base-release=")) experience.baseAgentReleaseId = token.slice(26);
8744
+ else if (token === "--experience-agent-definition") experience.agentDefinitionId = take();
8745
+ else if (token.startsWith("--experience-agent-definition=")) experience.agentDefinitionId = token.slice(30);
8746
+ else if (token === "--experience-task-signature") addList(experience.taskSignatures, take());
8747
+ else if (token.startsWith("--experience-task-signature=")) addList(experience.taskSignatures, token.slice(28));
8748
+ else if (token === "--experience-environment") addList(experience.environmentTags, take());
8749
+ else if (token.startsWith("--experience-environment=")) addList(experience.environmentTags, token.slice(25));
8750
+ else if (token === "--no-experience") experience.disabled = true;
8751
+ else prompt.push(token);
8752
+ }
8753
+ return { prompt: prompt.join(" "), experience };
8754
+ }
8755
+
8756
+ async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = null) {
8521
8757
  const agent = resolveAgent(db, query);
8522
8758
  if (!agent) {
8523
8759
  const routedPrompt = [query, prompt].filter(Boolean).join(" ").trim() || (await readStdin());
8524
8760
  if (!routedPrompt || !routedPrompt.trim()) fail("프롬프트가 비어 있습니다. agentlas run <agent> \"...\" 또는 agentlas run \"...\" 형식으로 입력하세요.");
8525
- return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride);
8761
+ return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride, runtimeExperience);
8526
8762
  }
8527
8763
  let userPrompt = prompt;
8528
8764
  if (!userPrompt) userPrompt = await readStdin();
8529
8765
  if (!userPrompt || !userPrompt.trim()) fail("프롬프트가 비어 있습니다. agentlas run <agent> \"...\" 또는 stdin으로 전달하세요.");
8530
8766
  process.stderr.write(`▸ ${agent.name}\n`);
8531
- const code = await executeOnce(db, agentSystemPromptCli(agent), userPrompt.trim(), runtimeOverride, { projectPath: activeProjectPath(db), agentId: agent.id, permission: PERMISSION });
8767
+ const code = await executeOnce(db, agentSystemPromptCli(agent), userPrompt.trim(), runtimeOverride, {
8768
+ projectPath: activeProjectPath(db), agentId: agent.id, permission: PERMISSION, runtimeExperience,
8769
+ });
8532
8770
  process.exit(code);
8533
8771
  }
8534
8772
 
8535
- async function cmdAutoRun(db, prompt, runtimeOverride) {
8773
+ async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null) {
8536
8774
  const lang = prefsLang();
8537
8775
  const choice = autoRouteAgent(db, prompt, lang);
8538
8776
  if (!choice) fail("자동 라우팅할 에이전트가 없습니다. agentlas list로 설치 상태를 확인하세요.");
8777
+ if (choice.direct) {
8778
+ // 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
8779
+ process.stderr.write(`▸ direct (no agent)\n`);
8780
+ process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
8781
+ const sys = `${autoRoutePreamble(choice, lang)}\n\n${directSystemPrompt(lang)}`;
8782
+ const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
8783
+ projectPath: activeProjectPath(db),
8784
+ agentId: null,
8785
+ permission: PERMISSION,
8786
+ runtimeExperience,
8787
+ });
8788
+ process.exit(code);
8789
+ }
8539
8790
  process.stderr.write(`▸ ${choice.agent.name} (auto)\n`);
8540
8791
  process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
8541
8792
  const sys = `${autoRoutePreamble(choice, lang)}\n\n${agentSystemPromptCli(choice.agent)}`;
@@ -8543,6 +8794,7 @@ async function cmdAutoRun(db, prompt, runtimeOverride) {
8543
8794
  projectPath: activeProjectPath(db),
8544
8795
  agentId: choice.agent.id,
8545
8796
  permission: PERMISSION,
8797
+ runtimeExperience,
8546
8798
  });
8547
8799
  process.exit(code);
8548
8800
  }
@@ -8603,7 +8855,7 @@ async function cmdFirm(db, query, prompt, runtimeOverride) {
8603
8855
  slug: firm.slug,
8604
8856
  label: firm.name + " CEO",
8605
8857
  system: sys,
8606
- capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
8858
+ capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, tagline_en: firm.tagline_en, entity_kind: "team", system_prompt: sys },
8607
8859
  };
8608
8860
  return launchTui(db, subject, runtimeOverride);
8609
8861
  }
@@ -9936,6 +10188,9 @@ function cmdHelp() {
9936
10188
  " connect [<sub>] wire Telegram / platforms to an agent team (hep-connect)",
9937
10189
  " import <path> import a local agent/team folder",
9938
10190
  " 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",
10193
+ " variant resolve local variant selection: selected|fallback|base-only|error",
9939
10194
  "",
9940
10195
  hdr("EXECUTE"),
9941
10196
  " storm <goal> force-robust pipeline: route → verify → execute (Stormbreaker) [--research]",
@@ -10038,8 +10293,10 @@ async function main() {
10038
10293
  return cmdImport(db, rest[1]);
10039
10294
  case "cd":
10040
10295
  return cmdCd(db, rest[1]);
10041
- case "run":
10042
- return cmdRun(db, rest[1], rest.slice(2).join(" "), runtimeOverride);
10296
+ case "run": {
10297
+ const runInput = parseRunExperienceArgs(rest.slice(2));
10298
+ return cmdRun(db, rest[1], runInput.prompt, runtimeOverride, runInput.experience);
10299
+ }
10043
10300
  case "chat":
10044
10301
  case "open":
10045
10302
  return cmdOpen(db, rest[1], runtimeOverride);
@@ -10074,8 +10331,37 @@ async function main() {
10074
10331
  return parity().cmdHep(db, rest.slice(1));
10075
10332
  // ── Agentlas OS 정식 표면 (hep-*) 1급 노출 ──
10076
10333
  case "build":
10077
- // hep-build "<요청>" 에이전트/팀 빌드 (Meta-Agent Factory)
10078
- return parity().cmdHep(db, rest.length > 1 ? ["hep-build", rest.slice(1).join(" ")] : ["hep-build"]);
10334
+ // Terminal-owned preflight: trusted system-global MCP metadata first, one consent,
10335
+ // then pass only approved catalog IDs/value-free shortages to the existing builder.
10336
+ return terminalAssets.cmdBuild({
10337
+ db,
10338
+ args: rest.slice(1),
10339
+ userDataDir: userDataDir(),
10340
+ cwd: projectCwd(),
10341
+ input: process.stdin,
10342
+ promptOutput: process.stderr,
10343
+ out,
10344
+ invokeBuild: (request) => parity().cmdHep(db, request ? ["hep-build", request] : ["hep-build"]),
10345
+ });
10346
+ case "experience":
10347
+ return terminalExperienceExchange.cmdExperienceExchange({
10348
+ args: rest.slice(1),
10349
+ userDataDir: userDataDir(),
10350
+ cwd: projectCwd(),
10351
+ out,
10352
+ env: process.env,
10353
+ getSessionCookie: cloudSessionCookieCli,
10354
+ fetchHub: (url, init) => fetchHubCli(url, init),
10355
+ legacyCommand: (legacyOptions) => terminalAssets.cmdExperience(legacyOptions),
10356
+ });
10357
+ case "variant":
10358
+ return terminalAssets.cmdVariant({
10359
+ db,
10360
+ args: rest.slice(1),
10361
+ userDataDir: userDataDir(),
10362
+ cwd: projectCwd(),
10363
+ out,
10364
+ });
10079
10365
  case "search": // hep-search — 에이전트 디렉터리 발견 (Hub + 로컬)
10080
10366
  if (!rest[1]) return fail('usage: agentlas search "<찾는 일>" [--limit 10]');
10081
10367
  return parity().cloudSearch(db, rest.slice(1));
@@ -10160,6 +10446,9 @@ module.exports = {
10160
10446
  parseDotEnvCli,
10161
10447
  isProtectedChildEnvKeyCli,
10162
10448
  mergeChildEnvValuesCli,
10449
+ openNodeSqliteDb,
10450
+ ensureMemoryContextColumn,
10451
+ writeJsonPrivateAtomicCli,
10163
10452
  resolveCredentialSourcePath,
10164
10453
  upsertEnvLine,
10165
10454
  fetchHubCli,
@@ -10193,6 +10482,12 @@ module.exports = {
10193
10482
  cloudPackageHashVersion,
10194
10483
  cloudPortablePathConflict,
10195
10484
  cloudPortableExecutableForFile,
10485
+ parseRunExperienceArgs,
10196
10486
  DEFAULT_API_MODEL,
10197
10487
  ANTHROPIC_COMPAT_API,
10488
+ // 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.
10489
+ autoRouteAgent,
10490
+ autoRouteNote,
10491
+ autoRoutePreamble,
10492
+ directSystemPrompt,
10198
10493
  };