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
@@ -1,180 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const assert = require("node:assert/strict");
5
- const fs = require("node:fs");
6
- const os = require("node:os");
7
- const path = require("node:path");
8
- const {
9
- claudeArgs,
10
- codexArgs,
11
- geminiArgs,
12
- prepareCodexRuntimeEnv,
13
- } = require("../engine/agentlas-native-host.cjs");
14
- const permissions = require("../engine/agentlas-permissions.cjs");
15
- const { buildArgs: legacyBuildArgs } = require("../engine/agentlas.cjs");
16
-
17
- const mcpServers = [{ name: "playwright", command: "npx", args: ["@playwright/mcp"] }];
18
-
19
- function hasPair(args, flag, value) {
20
- const index = args.indexOf(flag);
21
- return index >= 0 && args[index + 1] === value;
22
- }
23
-
24
- function includesExternalMcp(args) {
25
- if (args.some((arg) => String(arg).includes("mcp_servers"))) return true;
26
- for (let index = 0; index < args.length; index++) {
27
- if (args[index] !== "--mcp-config") continue;
28
- const value = String(args[index + 1] || "");
29
- try {
30
- const parsed = JSON.parse(value);
31
- if (Object.keys(parsed.mcpServers || {}).length > 0) return true;
32
- } catch {
33
- return true; // a generated config file is the explicit full-access inventory
34
- }
35
- }
36
- return false;
37
- }
38
-
39
- function common(level) {
40
- return {
41
- prompt: "test",
42
- systemPrompt: "system",
43
- permission: level,
44
- session: {},
45
- cwd: process.cwd(),
46
- mcpServers,
47
- };
48
- }
49
-
50
- function testClaude() {
51
- const read = claudeArgs(common("read"));
52
- const write = claudeArgs(common("write"));
53
- const full = claudeArgs(common("full"));
54
- assert.ok(hasPair(read, "--permission-mode", "plan"));
55
- assert.ok(hasPair(write, "--permission-mode", "acceptEdits"));
56
- assert.ok(full.includes("--dangerously-skip-permissions"));
57
- assert.ok(!write.includes("--dangerously-skip-permissions"), "write must never launch Claude unrestricted");
58
- for (const args of [read, write]) {
59
- assert.ok(args.includes("--strict-mcp-config"), "Claude read/write must ignore user/project MCP configuration");
60
- assert.equal(includesExternalMcp(args), false, "Claude read/write must receive an explicit empty MCP inventory");
61
- }
62
- assert.ok(full.includes("--strict-mcp-config"), "Claude full must use only the Agentlas-provided MCP inventory");
63
- assert.equal(includesExternalMcp(full), true);
64
- }
65
-
66
- function testCodex() {
67
- const read = codexArgs(common("read"));
68
- const write = codexArgs(common("write"));
69
- const full = codexArgs(common("full"));
70
- assert.ok(hasPair(read, "--sandbox", "read-only"));
71
- assert.ok(hasPair(write, "--sandbox", "workspace-write"));
72
- assert.ok(full.includes("--dangerously-bypass-approvals-and-sandbox"));
73
- assert.ok(!write.includes("--dangerously-bypass-approvals-and-sandbox"), "write must stay sandboxed");
74
- assert.equal(includesExternalMcp(read), false);
75
- assert.equal(includesExternalMcp(write), false, "write must not auto-inject Playwright or another external MCP");
76
- assert.equal(includesExternalMcp(full), true);
77
- }
78
-
79
- function testGemini() {
80
- const read = geminiArgs(common("read"));
81
- const write = geminiArgs(common("write"));
82
- const full = geminiArgs(common("full"));
83
- assert.ok(hasPair(read, "--approval-mode", "plan"));
84
- assert.ok(hasPair(write, "--approval-mode", "auto_edit"));
85
- assert.ok(hasPair(full, "--approval-mode", "yolo"));
86
- assert.equal(write.includes("--yolo"), false);
87
- for (const args of [read, write]) {
88
- const index = args.indexOf("--allowed-mcp-server-names");
89
- assert.ok(index >= 0 && /^__agentlas_no_mcp_[0-9a-f-]+__$/.test(String(args[index + 1])), "Gemini read/write must use an exclusive empty MCP allow-list");
90
- }
91
- assert.equal(full.includes("--allowed-mcp-server-names"), false, "Gemini full may use the user's explicitly configured MCP servers");
92
- }
93
-
94
- function testCodexIsolatedHome() {
95
- const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-codex-home-"));
96
- const source = path.join(fixture, "source");
97
- const data = path.join(fixture, "agentlas-data");
98
- fs.mkdirSync(source, { recursive: true });
99
- fs.writeFileSync(path.join(source, "auth.json"), '{"token":"fixture"}\n', { mode: 0o600 });
100
- fs.writeFileSync(path.join(source, "config.toml"), '[mcp_servers.victim]\nurl="https://victim.invalid"\n', "utf8");
101
- try {
102
- const isolated = prepareCodexRuntimeEnv({ CODEX_HOME: source, AGENTLAS_USER_DATA_DIR: data });
103
- assert.notEqual(isolated.CODEX_HOME, source);
104
- assert.equal(fs.readFileSync(path.join(isolated.CODEX_HOME, "auth.json"), "utf8"), '{"token":"fixture"}\n');
105
- assert.doesNotMatch(fs.readFileSync(path.join(isolated.CODEX_HOME, "config.toml"), "utf8"), /mcp_servers/);
106
- assert.match(fs.readFileSync(path.join(source, "config.toml"), "utf8"), /victim/);
107
- assert.throws(
108
- () => prepareCodexRuntimeEnv({ CODEX_HOME: source, AGENTLAS_USER_DATA_DIR: data, AGENTLAS_CODEX_HOME: source }),
109
- /must be isolated/,
110
- );
111
- assert.match(fs.readFileSync(path.join(source, "config.toml"), "utf8"), /victim/, "isolation failure overwrote the user's Codex config");
112
- } finally {
113
- fs.rmSync(fixture, { recursive: true, force: true });
114
- }
115
- }
116
-
117
- function testFailClosedAndCopy() {
118
- assert.equal(permissions.normalize("corrupt-value"), "read");
119
- const invalidCodex = codexArgs(common("corrupt-value"));
120
- assert.ok(hasPair(invalidCodex, "--sandbox", "read-only"));
121
- assert.equal(permissions.copy("write", "en").label, "workspace write");
122
- assert.match(permissions.copy("full", "ko").description, /승인과 샌드박스를 우회/);
123
- assert.deepEqual(permissions.LEVELS, ["read", "write", "full"]);
124
- }
125
-
126
- function testShiftTabFullConfirmation() {
127
- let clock = 1_000;
128
- const cycle = permissions.createCycleController({ now: () => clock, armMs: 5_000 });
129
- assert.deepEqual(cycle.step("read"), { level: "write", armed: false, enteredFull: false });
130
- assert.deepEqual(cycle.step("write"), { level: "write", armed: true, enteredFull: false });
131
- assert.equal(cycle.armed(), true);
132
- cycle.cancel();
133
- assert.equal(cycle.armed(), false, "any non-Shift-Tab key must disarm full escalation");
134
- assert.deepEqual(cycle.step("write"), { level: "write", armed: true, enteredFull: false });
135
- assert.deepEqual(cycle.step("write"), { level: "full", armed: false, enteredFull: true });
136
- assert.deepEqual(cycle.step("full"), { level: "read", armed: false, enteredFull: false });
137
- cycle.step("write");
138
- clock += 5_001;
139
- assert.deepEqual(cycle.step("write"), { level: "write", armed: true, enteredFull: false }, "expired arm must require a fresh double press");
140
- }
141
-
142
- function testBackgroundAndSwarmCapturePath() {
143
- for (const kind of ["claude-code", "codex", "gemini"]) {
144
- const read = legacyBuildArgs(kind, "system", "prompt", "read");
145
- const write = legacyBuildArgs(kind, "system", "prompt", "write");
146
- const full = legacyBuildArgs(kind, "system", "prompt", "full");
147
- assert.equal(includesExternalMcp(read), false, `${kind} read capture must not inject MCP`);
148
- assert.equal(includesExternalMcp(write), false, `${kind} write capture must not inject MCP`);
149
- if (kind !== "gemini") assert.equal(includesExternalMcp(full), true, `${kind} full capture should retain explicit Playwright access`);
150
- }
151
-
152
- const claudeWrite = legacyBuildArgs("claude-code", "system", "prompt", "write");
153
- const claudeFull = legacyBuildArgs("claude-code", "system", "prompt", "full");
154
- assert.ok(hasPair(claudeWrite, "--permission-mode", "acceptEdits"));
155
- assert.ok(claudeFull.includes("--dangerously-skip-permissions"));
156
-
157
- const codexWrite = legacyBuildArgs("codex", "system", "prompt", "write");
158
- const codexFull = legacyBuildArgs("codex", "system", "prompt", "full");
159
- assert.ok(hasPair(codexWrite, "--sandbox", "workspace-write"));
160
- assert.ok(!codexWrite.includes("--dangerously-bypass-approvals-and-sandbox"));
161
- assert.ok(codexFull.includes("--dangerously-bypass-approvals-and-sandbox"));
162
-
163
- const geminiWrite = legacyBuildArgs("gemini", "system", "prompt", "write");
164
- const geminiFull = legacyBuildArgs("gemini", "system", "prompt", "full");
165
- assert.ok(hasPair(geminiWrite, "--approval-mode", "auto_edit"));
166
- assert.ok(hasPair(geminiFull, "--approval-mode", "yolo"));
167
- assert.equal(geminiWrite.includes("--yolo"), false);
168
-
169
- const invalid = legacyBuildArgs("codex", "system", "prompt", "corrupt-value");
170
- assert.ok(hasPair(invalid, "--sandbox", "read-only"), "capture path must also fail closed");
171
- }
172
-
173
- testClaude();
174
- testCodex();
175
- testGemini();
176
- testCodexIsolatedHome();
177
- testFailClosedAndCopy();
178
- testShiftTabFullConfirmation();
179
- testBackgroundAndSwarmCapturePath();
180
- console.log("permission-mapping: PASS");
@@ -1,357 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /*
4
- * 자동 라우팅 회귀 테스트 — 2026-07-12 오라우팅 사고 고정.
5
- *
6
- * 사고: 일반 맥 질문("맥이 잠금상태에서 자꾸 ai 돌릴때 안켜지게 하는법 없나")이
7
- * "ai" 단어 하나(+2점)로 Pitch Deck Architect에 라우팅되고, 그 에이전트의 이미지
8
- * 힌트 때문에 런타임까지 gemini로 끌려갔다.
9
- * 수리: 초범용 토큰 스톱워드 + IDF 근사 필터 + 정체성 가중치 + 확신 임계값(MIN_ROUTE_SCORE)
10
- * 미만이면 direct(에이전트·능력 라우팅 없음) 판정.
11
- */
12
-
13
- const assert = require("node:assert/strict");
14
- const { autoRouteAgent, autoRouteNote, autoRoutePreamble, directSystemPrompt } = require("../engine/agentlas.cjs");
15
- const { needsImage, autoRuntimeFor } = require("../engine/agentlas-capabilities.cjs");
16
-
17
- // 실제 설치 상태를 흉내 낸 스텁 DB — 모든 에이전트 프롬프트에 "AI"가 들어 있다(현실과 동일).
18
- const AGENTS = [
19
- {
20
- id: "a1",
21
- slug: "pitch-deck-architect",
22
- name: "Pitch Deck Architect",
23
- name_en: "Pitch Deck Architect",
24
- tagline: "투자 유치용 피치덱 설계",
25
- tagline_en: "Investor pitch deck design",
26
- system_prompt: "You are an AI pitch deck architect. Design slides, visuals, images, 디자인, storytelling for investors.",
27
- },
28
- {
29
- id: "a2",
30
- slug: "thumbnail-studio",
31
- name: "썸네일 스튜디오",
32
- name_en: "Thumbnail Studio",
33
- tagline: "유튜브 썸네일 디자인",
34
- tagline_en: "YouTube thumbnail design",
35
- system_prompt: "You are an AI thumbnail designer. Generate 썸네일 images with bold typography.",
36
- },
37
- {
38
- id: "a3",
39
- slug: "agentlas-pm-soul",
40
- name: "PM Soul",
41
- name_en: "PM Soul",
42
- tagline: "프로젝트 연속성 관리",
43
- tagline_en: "Project continuity",
44
- system_prompt: "You are an AI project manager. Track decisions, plans, handoffs.",
45
- },
46
- {
47
- id: "a4",
48
- slug: "agentlas-memory-curator",
49
- name: "Memory Curator",
50
- name_en: "Memory Curator",
51
- tagline: "기억 저장/회상 관리",
52
- tagline_en: "Memory curation",
53
- system_prompt: "You are an AI memory curator. Store and recall durable memory entries.",
54
- },
55
- ];
56
- const META = [
57
- {
58
- id: "m1",
59
- slug: "agentlas-meta-agent",
60
- name: "메타에이전트",
61
- name_en: "Meta Agent",
62
- tagline: "에이전트/팀 빌더",
63
- tagline_en: "Agent/team builder",
64
- system_prompt: "You build new agents and teams.",
65
- },
66
- ];
67
- // 스텁 DB 팩토리 — 엔진의 쿼리 분기(/WHERE slug IN/ = 메타빌더 조회) 계약을 한 곳에 고정.
68
- function makeDb(agents, meta = []) {
69
- return { prepare: (sql) => ({ all: () => (/WHERE slug IN/.test(sql) ? meta : agents) }) };
70
- }
71
- const db = makeDb(AGENTS, META);
72
-
73
- // 1) 사고 재현 프롬프트 → direct (어떤 에이전트도, 특히 Pitch Deck Architect도 선택 금지)
74
- {
75
- const choice = autoRouteAgent(db, "맥이 잠금상태에서 자꾸 ai 돌릴때 안켜지게 하는법 없나", "ko");
76
- assert.equal(choice.direct, true, `일반 질문은 direct여야 함 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
77
- assert.equal(choice.agent, null);
78
- assert.match(autoRouteNote(choice, "ko"), /사용 에이전트: 없음/);
79
- assert.match(autoRouteNote(choice, "en"), /Selected agent: none/);
80
- assert.match(autoRoutePreamble(choice, "ko"), /direct answer/i);
81
- }
82
-
83
- // 2) "ai"/"llm" 단독 언급의 영어 일반 질문도 direct
84
- {
85
- const choice = autoRouteAgent(db, "how do I keep my mac from sleeping while ai jobs run", "en");
86
- assert.equal(choice.direct, true, "영어 일반 질문도 direct여야 함");
87
- }
88
-
89
- // 3) 정체성(이름/태그라인) 적중은 여전히 전문 라우트 — 썸네일 요청은 썸네일 에이전트로
90
- {
91
- const choice = autoRouteAgent(db, "유튜브 썸네일 하나 뽑아줘", "ko");
92
- assert.equal(choice.direct, undefined, "썸네일 요청이 direct로 새면 안 됨");
93
- assert.equal(choice.agent.slug, "thumbnail-studio");
94
- }
95
-
96
- // 4) 에이전트 이름을 직접 부르면 그 에이전트로
97
- {
98
- const choice = autoRouteAgent(db, "pitch deck 초안 잡아줘", "ko");
99
- assert.equal(choice.direct, undefined);
100
- assert.equal(choice.agent.slug, "pitch-deck-architect");
101
- }
102
-
103
- // 5) 빌드 의도는 메타빌더 직행 (기존 동작 유지)
104
- {
105
- const choice = autoRouteAgent(db, "인스타 카드뉴스 에이전트 하나 만들어줘", "ko");
106
- assert.equal(choice.agent.slug, "agentlas-meta-agent");
107
- assert.equal(choice.score, 1000);
108
- }
109
-
110
- // 6) 설치 에이전트가 없어도 direct로 답한다 (픽커 오류 대신)
111
- {
112
- const choice = autoRouteAgent(makeDb([]), "안녕 오늘 날씨 어때", "ko");
113
- assert.equal(choice.direct, true);
114
- }
115
-
116
- // 7) 직답 시스템 프롬프트 — 페르소나 없음, 양 언어 모두 존재
117
- assert.match(directSystemPrompt("ko"), /기본 어시스턴트/);
118
- assert.match(directSystemPrompt("en"), /default assistant/);
119
-
120
- // ── 2026-07-12 두 번째 오라우팅 사고 고정 ─────────────────────────────────────
121
- // 사고: "/Users/mason/Documents/법인관련/Appbridge_Template.이 양식으로 …" 프롬프트가
122
- // 경로 토큰("users","mason","documents","users-mason-documents-")으로 appbridge에 +2씩 쌓여
123
- // 라우팅 근거에까지 노출되고, appbridge CEO 프롬프트 속 지나가는 "디자인" 한 단어 때문에
124
- // needsImage가 참이 되어 PPT 요청 세션이 통째로 gemini로 전환됐다.
125
- const PATH_AGENTS = [
126
- {
127
- id: "p1",
128
- slug: "local-appbridge",
129
- name: "appbridge",
130
- name_en: "appbridge",
131
- tagline: "Imported local team",
132
- tagline_en: "Imported local team",
133
- system_prompt:
134
- "You are the AppBridge CEO coordination team imported from /Users/mason/Documents/Appbridge. " +
135
- "CEO는 코디네이션·라우팅의 owner다. 코드/디자인/스토어/보안 결정의 owner가 아니다. " +
136
- "Templates live under /Users/mason/Documents/Appbridge/templates (Appbridge_Template).",
137
- },
138
- {
139
- id: "p2",
140
- slug: "local-stock-team",
141
- name: "주식 팀",
142
- name_en: "Stock Team",
143
- tagline: "Imported local team",
144
- tagline_en: "Imported local team",
145
- system_prompt: "You trade stocks. Sources under /Users/mason/Documents/StockTeam. 리포트 디자인 지침을 따른다.",
146
- },
147
- ];
148
- const pathDb = makeDb(PATH_AGENTS);
149
-
150
- // 8) 사고 재현 — 이름을 실제로 부른 경로 프롬프트는 그 에이전트로 가되, 근거에 경로 쓰레기 토큰이 없어야 한다
151
- {
152
- const choice = autoRouteAgent(pathDb, "/Users/mason/Documents/법인관련/Appbridge_Template.이 양식으로 고정 시켜서 못만드나 피피티 잘만드는거..", "en");
153
- assert.equal(choice.direct, undefined, "Appbridge_Template을 직접 언급했으므로 appbridge 라우트 유지");
154
- assert.equal(choice.agent.slug, "local-appbridge");
155
- for (const junk of ["users", "mason", "documents", "users-mason-documents"]) {
156
- assert.ok(!choice.terms.some((t) => t.toLowerCase().includes(junk)), `경로 토큰 "${junk}"이 라우팅 근거에 노출되면 안 됨 — 실제: ${JSON.stringify(choice.terms)}`);
157
- }
158
- }
159
-
160
- // 9) 무관한 파일 경로 프롬프트 — 경로↔경로 우연 일치로 위임되면 안 된다 (direct)
161
- {
162
- const choice = autoRouteAgent(pathDb, "/Users/mason/Documents/법인관련/세금계산서.pdf 이거 요약해줘", "ko");
163
- assert.equal(choice.direct, true, `무관 경로 프롬프트는 direct여야 함 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
164
- }
165
-
166
- // 10) 약한 본문 단어 적중만으로는(strong 신호 없이) 절대 위임하지 않는다
167
- {
168
- const choice = autoRouteAgent(pathDb, "리포트 지침 owner 정리해줘 coordination 관점에서", "ko");
169
- assert.equal(choice.direct, true, `약한 본문 적중만으로 위임 금지 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
170
- }
171
-
172
- // 11) name === name_en 인 에이전트가 이름 보너스 +20을 두 번 받지 않는다 (점수 상한 검증)
173
- {
174
- const choice = autoRouteAgent(pathDb, "appbridge 팀 상태 알려줘", "ko");
175
- assert.equal(choice.agent.slug, "local-appbridge");
176
- assert.ok(choice.score < 40, `이름 중복 보너스 금지 — score ${choice.score} < 40 이어야 함`);
177
- }
178
-
179
- // 12) needsImage — 조율 CEO 프롬프트의 지나가는 "디자인" 한 단어로 이미지 판정 금지
180
- {
181
- assert.equal(needsImage(PATH_AGENTS[0]), false, "appbridge는 이미지 에이전트가 아님");
182
- assert.equal(needsImage(PATH_AGENTS[1]), false, "stock team은 이미지 에이전트가 아님");
183
- // 정체성 존(이름/태그라인)의 이미지 힌트는 그대로 신뢰
184
- assert.equal(
185
- needsImage({ slug: "thumbnail-studio", name: "썸네일 스튜디오", tagline: "유튜브 썸네일 디자인", system_prompt: "" }),
186
- true,
187
- "정체성 존 이미지 힌트는 유지",
188
- );
189
- // 본문 단독: 힌트를 포함한 "긍정문"이 3문장 이상이어야 이미지 판정
190
- assert.equal(
191
- needsImage({ slug: "s1", name: "스튜디오", tagline: "제작", system_prompt: "요청마다 이미지를 생성한다. 유튜브 썸네일을 만든다. 배너 시안을 뽑아 저장한다." }),
192
- true,
193
- "긍정문 3문장 이상이면 이미지 판정 유지",
194
- );
195
- // 겹치는 정규식 여러 개를 때리는 "한 문장"으로는 판정 금지 (상관 힌트 무력화)
196
- assert.equal(
197
- needsImage({ slug: "s2", name: "스튜디오", tagline: "제작", system_prompt: "이미지 생성 후 썸네일과 배너, 포스터를 만든다." }),
198
- false,
199
- "한 문장 안의 상관 힌트 여러 개는 1클러스터",
200
- );
201
- // 이미지 판정이 꺼졌으니 세션 런타임(claude-code)이 gemini로 전환되지 않는다
202
- assert.equal(
203
- autoRuntimeFor(PATH_AGENTS[0], { installedKinds: ["claude-code", "codex", "gemini"], activeSpec: "claude-code" }),
204
- "claude-code",
205
- "appbridge 세션이 gemini로 하이재킹되면 안 됨",
206
- );
207
- }
208
-
209
- // ── max 리뷰(2026-07-12)에서 실증된 잔여 결함 고정 ───────────────────────────
210
- // 13) 힌트 채널 비대칭 — 경로 디렉터리명("projects/plan")이 pm-soul strong 위임을 만들면 안 된다
211
- {
212
- const choice = autoRouteAgent(db, "/Users/mason/projects/plan/발표자료.pptx 열어서 요약해줘", "ko");
213
- assert.equal(choice.direct, true, `경로 힌트 위임 금지 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
214
- }
215
-
216
- // 14) 이름 채널 비대칭 — 부모 폴더명("…/Appbridge/…")만으로 +20 strong 위임 금지
217
- {
218
- const choice = autoRouteAgent(pathDb, "/Users/mason/Documents/Appbridge/세금계산서.pdf 이거 요약해줘", "ko");
219
- assert.equal(choice.direct, true, `부모 폴더명 위임 금지 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
220
- }
221
-
222
- // 15) 메타빌더 우회 — 경로 속 "agent-tools"가 빌드 의도(score 1000)로 둔갑하면 안 된다
223
- {
224
- const choice = autoRouteAgent(db, "/Users/mason/agent-tools/notes.md 요약본 만들어줘", "ko");
225
- assert.notEqual(choice.agent && choice.agent.slug, "agentlas-meta-agent", "경로 토큰이 메타빌더를 부르면 안 됨");
226
- assert.equal(choice.direct, true);
227
- // 진짜 빌드 의도는 여전히 메타빌더로 (test 5와 동일 경로 재확인)
228
- const build = autoRouteAgent(db, "인스타 카드뉴스 에이전트 하나 만들어줘", "ko");
229
- assert.equal(build.agent.slug, "agentlas-meta-agent");
230
- assert.equal(build.strong, true, "메타 직행 choice도 strong 계약을 지켜야 함");
231
- }
232
-
233
- // 16) 약점수 1위 가림 — 장황한 약한 적중이 점수 1위여도, strong 자격자가 위임을 받는다
234
- {
235
- const noisy = [
236
- {
237
- id: "w1", slug: "verbose-ops", name: "운영 도우미", name_en: "Ops Helper", tagline: "운영", tagline_en: "ops",
238
- system_prompt: "youtube channel upload schedule traffic metrics publish calendar checklist 관리 매뉴얼 ".repeat(3),
239
- },
240
- {
241
- id: "s1", slug: "banner-studio", name: "배너 스튜디오", name_en: "Banner Studio", tagline: "썸네일 배너 디자인", tagline_en: "thumbnail banner design",
242
- system_prompt: "유튜브 썸네일과 배너를 디자인한다.",
243
- },
244
- ];
245
- const choice = autoRouteAgent(makeDb(noisy), "youtube channel upload schedule traffic metrics publish calendar checklist 썸네일 배너 만들어줘", "ko");
246
- assert.equal(choice.direct, undefined, "strong 자격자가 있는데 직답으로 새면 안 됨");
247
- assert.equal(choice.agent.slug, "banner-studio");
248
- }
249
-
250
- // 17) 공백 포함 macOS 경로("Mobile Documents")도 junk 토큰 없이 접힌다
251
- {
252
- const choice = autoRouteAgent(
253
- pathDb,
254
- "/Users/mason/Library/Mobile Documents/com~apple~CloudDocs/Appbridge_Template.md 이 양식으로 appbridge 정리해줘",
255
- "en",
256
- );
257
- assert.equal(choice.agent.slug, "local-appbridge");
258
- for (const junk of ["mobile", "documents", "com", "apple", "users", "mason", "library"]) {
259
- assert.ok(!choice.terms.some((t) => t.toLowerCase() === junk), `공백 경로 토큰 "${junk}" 노출 금지 — 실제: ${JSON.stringify(choice.terms)}`);
260
- }
261
- }
262
-
263
- // 18) 임포터 보일러플레이트 태그라인("Imported local team")은 정체성 신호가 아니다 (IDF 꺼지는 소규모 설치)
264
- {
265
- const choice = autoRouteAgent(makeDb(PATH_AGENTS), "local imported 항목 정리해줘", "ko");
266
- assert.equal(choice.direct, true, `보일러플레이트 위임 금지 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
267
- // 'team'은 아무 임포트 팀 slug의 부분문자열(+6 strong)이라 스톱워드여야 한다:
268
- // 스톱워드에서 빠지면 'team' 정체성 적중 + 약한 본문 적중으로 10을 넘어 위임된다.
269
- const teamProbe = autoRouteAgent(makeDb(PATH_AGENTS), "team 리포트 디자인 지침 정리해줘", "ko");
270
- assert.equal(teamProbe.direct, true, `범용어 'team' 위임 금지 — 실제: ${JSON.stringify(teamProbe.agent && teamProbe.agent.slug)}`);
271
- }
272
-
273
- // 18b) 상대경로 파일 참조의 디렉터리명("plan")이 힌트 strong 채널을 때리면 안 된다
274
- {
275
- const choice = autoRouteAgent(db, "docs/plan/roadmap.md 열어서 정리해줘", "ko");
276
- assert.equal(choice.direct, true, `상대경로 힌트 위임 금지 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
277
- }
278
-
279
- // 18c) 파일명 속 "agent"는 빌드 의도가 아니다 — 산문 빌드 의도만 메타빌더로
280
- {
281
- const file = autoRouteAgent(db, "agent-notes.md 요약본 만들어줘", "ko");
282
- assert.notEqual(file.agent && file.agent.slug, "agentlas-meta-agent", "파일명 토큰이 메타빌더를 부르면 안 됨");
283
- const rel = autoRouteAgent(db, "agent-tools/notes/summary.md 초안 만들어줘", "ko");
284
- assert.notEqual(rel.agent && rel.agent.slug, "agentlas-meta-agent", "상대경로 토큰이 메타빌더를 부르면 안 됨");
285
- }
286
-
287
- // 18d) 공백 병합은 대문자 세그먼트("Mobile Documents")만 — 한글 프로즈를 경로로 삼키지 않는다
288
- {
289
- const choice = autoRouteAgent(makeDb(PATH_AGENTS), "지금 /tmp/out 확인하고 기획/디자인 관련 파일 목록 정리해줘", "ko");
290
- assert.equal(choice.direct, true);
291
- // 프로즈 토큰("기획","디자인")이 라우팅 어휘에서 사라지지 않았는지 — 스텁에 디자인 전문가를 넣어 확인
292
- const designers = [
293
- { id: "d1", slug: "design-desk", name: "디자인 데스크", name_en: "Design Desk", tagline: "기획 디자인 전문", tagline_en: "design", system_prompt: "기획과 디자인 자료를 정리한다." },
294
- ];
295
- const kept = autoRouteAgent(makeDb(designers), "지금 /tmp/out 확인하고 기획/디자인 관련 파일 목록 정리해줘", "ko");
296
- assert.equal(kept.direct, undefined, "프로즈 '기획/디자인'이 경로로 삼켜지면 안 됨");
297
- assert.equal(kept.agent.slug, "design-desk");
298
- }
299
-
300
- // 19) needsImage 정밀도 — 부정문·그림자·기계 파생 slug는 이미지 판정 금지, 도구 마커는 단독 인정
301
- {
302
- assert.equal(
303
- needsImage({ slug: "coord", name: "코디네이터", tagline: "조율", system_prompt: "상품 이미지 생성 금지. 코드 리뷰와 배포만 담당한다." }),
304
- false,
305
- "부정문(금지)의 힌트는 능력이 아님",
306
- );
307
- // 긍정문에 흔한 보조 부정("묻지 않고 바로 …")까지 부정으로 오판하면 안 된다
308
- assert.equal(
309
- needsImage({ slug: "fastgen", name: "생성기", tagline: "콘텐츠", system_prompt: "묻지 않고 바로 이미지를 생성한다. 요청 즉시 썸네일을 뽑는다. 지체 없이 배너를 만든다." }),
310
- true,
311
- "보조 부정이 낀 긍정문은 능력으로 인정",
312
- );
313
- assert.equal(
314
- needsImage({ slug: "fx", name: "이펙트 코더", tagline: "CSS 전문", system_prompt: "그림자 효과를 코드로 구현한다. 그림자 블러를 조정한다. 그림자 색을 계산한다." }),
315
- false,
316
- "'그림자'(shadow)는 이미지 힌트가 아님",
317
- );
318
- assert.equal(
319
- needsImage({ slug: "local-design-system", name: "토큰 린터", tagline: "코드 린트", system_prompt: "Lint CSS variables and tokens." }),
320
- false,
321
- "폴더명 파생 slug('design-system')는 단독 신뢰 대상이 아님",
322
- );
323
- assert.equal(
324
- needsImage({ slug: "gen", name: "제너레이터", tagline: "콘텐츠 제작", system_prompt: "결과물은 nano-banana로 렌더링해 저장한다." }),
325
- true,
326
- "이미지 도구 마커는 단독으로도 인정",
327
- );
328
- // 팀 CEO 두뇌는 body 채널을 신뢰하지 않는다 — 부서명("Design HQ")이 몇 문장 나와도
329
- // entity_kind='team'이면 정체성 존만 본다. 같은 본문이라도 단일 에이전트면 body로 판정.
330
- const orgBody = "디자인 부서가 배너를 만든다. 디자인 부서가 썸네일을 만든다. 디자인 부서가 포스터를 만든다.";
331
- assert.equal(
332
- needsImage({ slug: "eng-team", name: "엔지니어링 팀", tagline: "제품 개발", entity_kind: "team", system_prompt: orgBody }),
333
- false,
334
- "팀은 body 키워드로 이미지 판정 금지 (vibecoder 사례)",
335
- );
336
- assert.equal(
337
- needsImage({ slug: "solo", name: "제작기", tagline: "콘텐츠", entity_kind: "agent", system_prompt: orgBody }),
338
- true,
339
- "단일 에이전트는 body 클러스터 판정 유지",
340
- );
341
- }
342
-
343
- // 20) 슬래시로 붙은 엔티티도 빌드 의도로 인식 — 슬래시 토큰 통삭제 회귀 수리(2026-07-12 max 리뷰)
344
- // 사고: isAgentBuildIntent가 `\S*[\\/]\S*`로 슬래시 포함 토큰을 통째로 지워
345
- // "에이전트/팀 만들어줘"의 엔티티가 사라져 빌드 의도를 놓치고 direct로 샜다.
346
- {
347
- const b1 = autoRouteAgent(db, "에이전트/팀 하나 만들어줘", "ko");
348
- assert.equal(b1.agent && b1.agent.slug, "agentlas-meta-agent", "슬래시-엔티티('에이전트/팀')도 빌드 의도");
349
- const b2 = autoRouteAgent(db, "회사/조직 만들어줘", "ko");
350
- assert.equal(b2.agent && b2.agent.slug, "agentlas-meta-agent", "'회사/조직'도 빌드 의도");
351
- // 경로/파일 참조는 여전히 빌드 의도가 아니다 — 수리가 test 15의 회귀를 되살리지 않았는지 재확인
352
- const nf = autoRouteAgent(db, "/Users/mason/agent-tools/notes.md 요약본 만들어줘", "ko");
353
- assert.notEqual(nf.agent && nf.agent.slug, "agentlas-meta-agent", "경로 속 'agent-tools'는 빌드 아님");
354
- assert.equal(nf.direct, true);
355
- }
356
-
357
- console.log("route-regression: PASS");