agentlas 0.9.4 → 0.9.6

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.6 — 2026-07-25
4
+
5
+ - Agent and App Builder routing is now decided by the connected model: lexical
6
+ scores only recruit candidates, and the model can route a request the keyword
7
+ lists never matched (any language). The App Builder consent handshake is
8
+ unchanged, and every route receipt says whether the connected model or the
9
+ deterministic fallback decided.
10
+ - Whether an agent produces images (and therefore which runtime runs it) is now
11
+ judged by the connected model from the agent's own identity, with the old
12
+ keyword list demoted to reference hints. Conservative non-image vetoes stay.
13
+ - Task classification, routing, and image judgments all fail over to the
14
+ previous deterministic behavior — explicitly labeled — when no connected
15
+ model is available.
16
+ - Publication gates pin the Agentlas OS v1.1.61 runtime commit, which ships the
17
+ same judgment-engine migration across the bundled Core runtime.
18
+
3
19
  ## 0.9.4 — 2026-07-23
4
20
 
5
21
  - `plugin add` no longer registers a code-hosting page (GitHub/GitLab/Bitbucket
@@ -63,7 +63,10 @@ const IMAGE_TOOL_MARKERS = [/nano-?banana/i, /\bimagen\b/i, /gpt-image/i, /grok\
63
63
  // 겹치는 정규식 여러 개를 동시에 때려도 1클러스터다.
64
64
  const MIN_BODY_IMAGE_SENTENCES = 3;
65
65
  const BODY_SCAN_CAP = 16000; // 로컬 임포트 상한과 동일 — 클라우드 무제한 프롬프트의 전문 스캔 방지
66
- function needsImage(agent) {
66
+ // 어휘 판정 — 연결 모델이 없을 때의 결정적 폴백이자, 모델 판정 전의 참고 프리필터.
67
+ // 하우스 룰: 단어목록은 최종 결정을 내리지 않는다. 최종 판정은 resolveNeedsImage가
68
+ // 상주 판정 서비스(judgeLabels)에 묻고, 이 함수는 그 폴백으로만 살아남는다.
69
+ function needsImageLexical(agent) {
67
70
  if (!agent) return false;
68
71
  if (NON_IMAGE_ROLES.has(String(agent.role || "").toLowerCase())) return false;
69
72
  // 정체성 존은 사용자가 선언한 이름/태그라인만 — slug는 폴더명에서 기계 파생되므로
@@ -87,6 +90,82 @@ function needsImage(agent) {
87
90
  return false;
88
91
  }
89
92
 
93
+ // ── 상주 판정 서비스 배선 — 모델이 의미로 최종 판정, IMAGE_HINTS는 참고 힌트 ──────
94
+ // needsImage 호출자(REPL 배지·autoRuntimeFor·routingNote)는 동기라서 warm-cache 패턴:
95
+ // 비동기 경로(resolveNeedsImage)가 먼저 판정해 캐시를 데우고, 동기 needsImage는 캐시만
96
+ // 읽는다. 캐시 미스 = 어휘 폴백 그대로 — imageJudgmentSource가 어느 쪽이었는지 라벨한다.
97
+ const IMAGE_VERDICT_CACHE_MAX = 200;
98
+ const imageVerdicts = new Map();
99
+ function imageJudgeInput(agent) {
100
+ const identity = [agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].filter(Boolean).join(" | ");
101
+ return `${identity}\n---\n${String(agent.system_prompt || "").slice(0, 6000)}`;
102
+ }
103
+ // 역할/팀 베토는 보수적 하드 가드로 유지 — 조율 두뇌가 부서 소개 문장("Design HQ")으로
104
+ // 이미지 팀이 되던 사고(vibecoder/appbridge)는 모델 판정 대상에서 아예 뺀다.
105
+ function imageJudgeVetoed(agent) {
106
+ if (!agent) return true;
107
+ if (NON_IMAGE_ROLES.has(String(agent.role || "").toLowerCase())) return true;
108
+ if (String(agent.entity_kind || "").toLowerCase() === "team") return true;
109
+ return false;
110
+ }
111
+ // 이 에이전트의 직무가 이미지 생산인지를 연결 모델이 의미로 판정한다.
112
+ // 러너 없음/타임아웃/정크 → 어휘 판정을 "fallback"으로 라벨해 반환 (조용한 회귀 금지).
113
+ async function resolveNeedsImage(agent) {
114
+ const lexical = needsImageLexical(agent);
115
+ if (imageJudgeVetoed(agent)) return { image: lexical, source: "deterministic" };
116
+ let judgment;
117
+ try {
118
+ judgment = require("./agentlas-judgment.cjs");
119
+ } catch {
120
+ judgment = null;
121
+ }
122
+ if (!judgment || !judgment.hasJudgmentRunner()) return { image: lexical, source: "fallback" };
123
+ const input = imageJudgeInput(agent);
124
+ const cached = imageVerdicts.get(input);
125
+ if (cached) return cached;
126
+ const verdict = await judgment.judgeLabels({
127
+ kind: "agent-produces-images",
128
+ question:
129
+ "Does this agent's OWN job include producing images (generating or designing visual assets such as thumbnails, banners, logos, posters, product shots)?",
130
+ labels: ["image", "not-image"],
131
+ multi: false,
132
+ input,
133
+ hints: { image: IMAGE_HINTS.map((re) => re.source) },
134
+ guidance:
135
+ "Judge the agent's role from its identity and instructions, in any language. Mentioning images is not " +
136
+ "producing them: builders, orchestrators, PMs, curators, and coordination brains that commission or " +
137
+ "delegate image work are 'not-image'. Refusals or prohibitions ('never generate images') declare the " +
138
+ "opposite of a capability.",
139
+ fallback: [lexical ? "image" : "not-image"],
140
+ });
141
+ if (verdict.source !== "llm" || !verdict.labels.length) return { image: lexical, source: "fallback" };
142
+ const out = { image: verdict.labels[0] === "image", source: "llm", reason: verdict.reason || "" };
143
+ imageVerdicts.set(input, out);
144
+ if (imageVerdicts.size > IMAGE_VERDICT_CACHE_MAX) {
145
+ const oldest = imageVerdicts.keys().next().value;
146
+ if (oldest !== undefined) imageVerdicts.delete(oldest);
147
+ }
148
+ return out;
149
+ }
150
+ // Does this agent's job involve generating/handling images? Sync surface for badges and
151
+ // autoRuntimeFor: model verdict from the warm cache wins; miss = deterministic lexical fallback.
152
+ function needsImage(agent) {
153
+ if (!agent) return false;
154
+ if (!imageJudgeVetoed(agent)) {
155
+ const cached = imageVerdicts.get(imageJudgeInput(agent));
156
+ if (cached) return cached.image;
157
+ }
158
+ return needsImageLexical(agent);
159
+ }
160
+ // 라벨용 — 이 에이전트의 현재 이미지 판정이 모델("llm")인지 결정적 경로("deterministic")인지.
161
+ function imageJudgmentSource(agent) {
162
+ if (agent && !imageJudgeVetoed(agent) && imageVerdicts.has(imageJudgeInput(agent))) return "llm";
163
+ return "deterministic";
164
+ }
165
+ function clearImageJudgments() {
166
+ imageVerdicts.clear();
167
+ }
168
+
90
169
  // Auto-pick a runtime spec for an agent given installed CLI kinds and the session default spec.
91
170
  // Image agents route to an installed image-capable runtime; otherwise keep the session default.
92
171
  function autoRuntimeFor(agent, { installedKinds, activeSpec }) {
@@ -103,4 +182,17 @@ function badge(spec) {
103
182
  return c.image ? "🖼" : "";
104
183
  }
105
184
 
106
- module.exports = { RUNTIME_CAPS, CLI_KINDS, capsFor, specOf, runtimeFromSpec, needsImage, autoRuntimeFor, badge };
185
+ module.exports = {
186
+ RUNTIME_CAPS,
187
+ CLI_KINDS,
188
+ capsFor,
189
+ specOf,
190
+ runtimeFromSpec,
191
+ needsImage,
192
+ needsImageLexical,
193
+ resolveNeedsImage,
194
+ imageJudgmentSource,
195
+ clearImageJudgments,
196
+ autoRuntimeFor,
197
+ badge,
198
+ };
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+
3
+ // `agentlas evolve [list|apply <id>|revert <id>]` — Phase 2 / 2+ (terminal 표면).
4
+ //
5
+ // 데스크탑 트리거가 만든 "성장 제안"을 공유 agentlas.sqlite에서 읽어 검토·적용·되돌린다.
6
+ // hep/터미널 세션은 UI가 없으므로 이 명령이 4표면 발화 UX의 터미널 창구다.
7
+ // list — 대기 중(고위험 candidate) + 자동적용(저위험) 제안을 사람이 읽는 3줄로.
8
+ // apply <id> — 고위험 candidate를 명시 승인해 프롬프트에 적용(런타임 authority=system_prompt + 파일).
9
+ // revert <id> — 적용된 제안을 되돌린다.
10
+ //
11
+ // 적용/되돌리기 게이트는 도구 무관한 "타깃 파일 내용 해시(before_hash/after_hash)"로만 판정한다
12
+ // (데스크탑이 생성 시 저장한 sha256). 그래서 데스크탑이 자동적용한 저위험 제안도 터미널에서
13
+ // 안전하게 되돌릴 수 있다. 어떤 실패도 폴백 없이 상태로 노출한다.
14
+
15
+ const fs = require("node:fs");
16
+ const path = require("node:path");
17
+ const { createHash, randomUUID } = require("node:crypto");
18
+
19
+ function sha256(content) {
20
+ return createHash("sha256").update(String(content), "utf8").digest("hex");
21
+ }
22
+
23
+ const ABSENT_TARGET_HASH = sha256("agentlas:absent-agent-asset:v1");
24
+ const RULE_TARGETS = new Set([
25
+ "system-prompt.md",
26
+ "soul.md",
27
+ "agent.md",
28
+ "claude.md",
29
+ "agents.md",
30
+ "gemini.md",
31
+ "persona.md",
32
+ "prompt.md",
33
+ ]);
34
+
35
+ function parseSource(json) {
36
+ try {
37
+ const parsed = JSON.parse(json || "{}");
38
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
39
+ } catch {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ /** 대기 중(사람 결정 필요) 고위험 성장 제안 개수 — 터미널 홈 배너용. */
45
+ function countPendingGrowthProposals(db) {
46
+ try {
47
+ const row = db
48
+ .prepare(
49
+ `SELECT COUNT(*) AS n FROM agent_evolution_proposals
50
+ WHERE json_extract(source_json, '$._growth') = 1 AND status = 'candidate'`,
51
+ )
52
+ .get();
53
+ return Number(row && row.n ? row.n : 0);
54
+ } catch {
55
+ return 0;
56
+ }
57
+ }
58
+
59
+ function listGrowthProposals(db, limit = 50) {
60
+ let rows = [];
61
+ try {
62
+ rows = db
63
+ .prepare(
64
+ `SELECT * FROM agent_evolution_proposals
65
+ WHERE json_extract(source_json, '$._growth') = 1
66
+ AND status IN ('candidate','applied','measured')
67
+ ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC
68
+ LIMIT ?`,
69
+ )
70
+ .all(limit);
71
+ } catch {
72
+ rows = [];
73
+ }
74
+ const pending = [];
75
+ const autoApplied = [];
76
+ for (const row of rows) {
77
+ const source = parseSource(row.source_json);
78
+ const entry = { row, source };
79
+ if (row.status === "candidate") pending.push(entry);
80
+ else if (source._autoApplied === true) autoApplied.push(entry);
81
+ }
82
+ return { pending, autoApplied };
83
+ }
84
+
85
+ function agentById(db, id) {
86
+ try {
87
+ return db.prepare("SELECT * FROM installed_agents WHERE id = ?").get(id) || null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function targetFilePath(agentFolder, agent, targetPath) {
94
+ const dir = agentFolder(agent);
95
+ const safe = path.resolve(dir, targetPath);
96
+ if (safe !== path.join(dir, targetPath) && !safe.startsWith(path.resolve(dir) + path.sep)) {
97
+ throw new Error("Evolution target escapes the agent folder");
98
+ }
99
+ return safe;
100
+ }
101
+
102
+ function currentTargetHash(file) {
103
+ try {
104
+ const content = fs.readFileSync(file, "utf8");
105
+ return { exists: true, content, hash: sha256(content) };
106
+ } catch (error) {
107
+ if (error && error.code === "ENOENT") return { exists: false, content: "", hash: ABSENT_TARGET_HASH };
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ function printCard(out, entry, index) {
113
+ const { row, source } = entry;
114
+ const card = source.humanCard && typeof source.humanCard === "object" ? source.humanCard : null;
115
+ const tier = source.riskTier === "high" ? "high" : "low";
116
+ out(` [${index}] ${row.id} (${tier} · ${row.status} · ${row.agent_id})`);
117
+ if (card) {
118
+ out(` 배운 것 : ${card.learned}`);
119
+ out(` 바뀌는 것: ${card.change}`);
120
+ out(` 되돌리기 : ${card.reversible}`);
121
+ } else {
122
+ out(` ${row.summary}`);
123
+ }
124
+ }
125
+
126
+ function cmdList(db, out) {
127
+ const { pending, autoApplied } = listGrowthProposals(db);
128
+ out("== agent growth proposals ==");
129
+ out("");
130
+ out(`대기(승인 필요) ${pending.length}건:`);
131
+ if (!pending.length) out(" (없음)");
132
+ pending.forEach((entry, i) => printCard(out, entry, i + 1));
133
+ out("");
134
+ out(`자동 적용됨(저위험) ${autoApplied.length}건:`);
135
+ if (!autoApplied.length) out(" (없음)");
136
+ autoApplied.forEach((entry, i) => printCard(out, entry, i + 1));
137
+ out("");
138
+ out("적용: agentlas evolve apply <id> · 되돌리기: agentlas evolve revert <id>");
139
+ }
140
+
141
+ function loadProposal(db, id, fail) {
142
+ const row = db.prepare("SELECT * FROM agent_evolution_proposals WHERE id = ?").get(id);
143
+ if (!row) return fail(`Proposal not found: ${id}`);
144
+ if (row.proposal_type !== "rule") {
145
+ return fail(`Terminal evolve applies rule (prompt) proposals only; ${id} is '${row.proposal_type}'. Use the desktop app.`);
146
+ }
147
+ if (!RULE_TARGETS.has(String(row.target_path).toLowerCase())) {
148
+ return fail(`Unsupported evolution target: ${row.target_path}`);
149
+ }
150
+ return row;
151
+ }
152
+
153
+ // 터미널이 부트스트랩한 DB는 v45라 진화 영수증 테이블이 없을 수 있다(데스크탑 v51+ 마이그레이션이
154
+ // 추가). 앱과 동일한 스키마로 idempotent 생성 — 앱이 이미 만들었으면 no-op.
155
+ function ensureReceiptTable(db) {
156
+ db.exec(`
157
+ CREATE TABLE IF NOT EXISTS agent_evolution_receipts (
158
+ id TEXT PRIMARY KEY,
159
+ proposal_id TEXT NOT NULL,
160
+ agent_id TEXT NOT NULL,
161
+ action TEXT NOT NULL,
162
+ target_path TEXT NOT NULL,
163
+ version_before INTEGER NOT NULL,
164
+ version_after INTEGER NOT NULL,
165
+ target_hash_before TEXT NOT NULL,
166
+ target_hash_after TEXT NOT NULL,
167
+ package_hash_before TEXT NOT NULL,
168
+ package_hash_after TEXT NOT NULL,
169
+ created_at TEXT NOT NULL,
170
+ UNIQUE(proposal_id, action)
171
+ );
172
+ `);
173
+ }
174
+
175
+ function insertReceipt(db, row, action, hashBefore, hashAfter, now) {
176
+ ensureReceiptTable(db);
177
+ db.prepare(
178
+ `INSERT INTO agent_evolution_receipts (
179
+ id, proposal_id, agent_id, action, target_path,
180
+ version_before, version_after, target_hash_before, target_hash_after,
181
+ package_hash_before, package_hash_after, created_at
182
+ ) VALUES (?, ?, ?, ?, ?, 1, 2, ?, ?, ?, ?, ?)
183
+ ON CONFLICT(proposal_id, action) DO NOTHING`,
184
+ ).run(
185
+ `evo_receipt_${randomUUID()}`,
186
+ row.id,
187
+ row.agent_id,
188
+ action,
189
+ row.target_path,
190
+ hashBefore,
191
+ hashAfter,
192
+ hashBefore,
193
+ hashAfter,
194
+ now,
195
+ );
196
+ }
197
+
198
+ function cmdApply(db, id, out, fail, agentFolder) {
199
+ const row = loadProposal(db, id, fail);
200
+ if (!row) return;
201
+ if (row.status !== "candidate") {
202
+ return fail(`Only a pending candidate can be applied; ${id} is '${row.status}'.`);
203
+ }
204
+ const agent = agentById(db, row.agent_id);
205
+ if (!agent) return fail(`Agent not found for proposal: ${row.agent_id}`);
206
+ const file = targetFilePath(agentFolder, agent, row.target_path);
207
+ const current = currentTargetHash(file);
208
+ if (current.hash !== row.before_hash) {
209
+ return fail("Agent prompt changed after this proposal was created; review it in the desktop app and re-propose.");
210
+ }
211
+ const now = new Date().toISOString();
212
+ fs.mkdirSync(path.dirname(file), { recursive: true });
213
+ fs.writeFileSync(file, row.after_content, "utf8");
214
+ const verify = sha256(fs.readFileSync(file, "utf8"));
215
+ if (verify !== row.after_hash) {
216
+ // 원상복구 후 실패 노출(폴백 금지).
217
+ fs.writeFileSync(file, row.before_content, "utf8");
218
+ return fail("Applied content did not match the approved hash; restored the original.");
219
+ }
220
+ const tx = db.transaction(() => {
221
+ db.prepare("UPDATE installed_agents SET system_prompt = ? WHERE id = ?").run(row.after_content, row.agent_id);
222
+ insertReceipt(db, row, "apply", row.before_hash, row.after_hash, now);
223
+ db.prepare(
224
+ `UPDATE agent_evolution_proposals
225
+ SET status = 'applied', applied_at = COALESCE(applied_at, ?),
226
+ last_error = NULL, updated_at = ?
227
+ WHERE id = ? AND status = 'candidate'`,
228
+ ).run(now, now, row.id);
229
+ });
230
+ tx();
231
+ out(`applied ${id} → ${row.target_path} (agent ${row.agent_id}). Revert with: agentlas evolve revert ${id}`);
232
+ }
233
+
234
+ function cmdRevert(db, id, out, fail, agentFolder) {
235
+ const row = loadProposal(db, id, fail);
236
+ if (!row) return;
237
+ if (row.status !== "applied" && row.status !== "measured") {
238
+ return fail(`Only an applied proposal can be reverted; ${id} is '${row.status}'.`);
239
+ }
240
+ const applyReceipt = db
241
+ .prepare("SELECT 1 FROM agent_evolution_receipts WHERE proposal_id = ? AND action = 'apply' LIMIT 1")
242
+ .get(row.id);
243
+ if (!applyReceipt) return fail("Revert requires the verified apply receipt.");
244
+ const agent = agentById(db, row.agent_id);
245
+ if (!agent) return fail(`Agent not found for proposal: ${row.agent_id}`);
246
+ const file = targetFilePath(agentFolder, agent, row.target_path);
247
+ const current = currentTargetHash(file);
248
+ if (current.hash !== row.after_hash) {
249
+ return fail("Agent prompt changed after this proposal was applied; revert blocked to avoid clobbering newer edits.");
250
+ }
251
+ const now = new Date().toISOString();
252
+ fs.writeFileSync(file, row.before_content, "utf8");
253
+ const verify = sha256(fs.readFileSync(file, "utf8"));
254
+ if (verify !== row.before_hash) {
255
+ fs.writeFileSync(file, row.after_content, "utf8");
256
+ return fail("Reverted content did not match the original hash; restored the applied version.");
257
+ }
258
+ const tx = db.transaction(() => {
259
+ db.prepare("UPDATE installed_agents SET system_prompt = ? WHERE id = ?").run(row.before_content, row.agent_id);
260
+ insertReceipt(db, row, "rollback", row.after_hash, row.before_hash, now);
261
+ db.prepare(
262
+ `UPDATE agent_evolution_proposals
263
+ SET status = 'rolled_back', rolled_back_at = COALESCE(rolled_back_at, ?),
264
+ last_error = NULL, updated_at = ?
265
+ WHERE id = ? AND status IN ('applied','measured')`,
266
+ ).run(now, now, row.id);
267
+ });
268
+ tx();
269
+ out(`reverted ${id} → restored ${row.target_path} (agent ${row.agent_id}).`);
270
+ }
271
+
272
+ /**
273
+ * cmdEvolve — `agentlas evolve <sub> ...`.
274
+ * @param {{db:any,args:string[],out:(s:string)=>void,fail:(s:string)=>void,agentFolder:(a:any)=>string}} ctx
275
+ */
276
+ function cmdEvolve(ctx) {
277
+ const { db, out, fail, agentFolder } = ctx;
278
+ const args = Array.isArray(ctx.args) ? ctx.args : [];
279
+ const sub = args[0] || "list";
280
+ if (sub === "help" || sub === "--help" || sub === "-h") {
281
+ out("usage: agentlas evolve [list | apply <id> | revert <id>]");
282
+ out(" list — review pending (approval-needed) and auto-applied agent growth proposals");
283
+ out(" apply <id> — approve and apply a pending proposal to the agent prompt");
284
+ out(" revert <id> — roll back an applied proposal");
285
+ return;
286
+ }
287
+ if (sub === "list" || sub === "ls") return cmdList(db, out);
288
+ if (sub === "apply") {
289
+ if (!args[1]) return fail("usage: agentlas evolve apply <id>");
290
+ return cmdApply(db, args[1], out, fail, agentFolder);
291
+ }
292
+ if (sub === "revert" || sub === "rollback") {
293
+ if (!args[1]) return fail("usage: agentlas evolve revert <id>");
294
+ return cmdRevert(db, args[1], out, fail, agentFolder);
295
+ }
296
+ return fail(`Unknown evolve subcommand: ${sub} (list|apply|revert)`);
297
+ }
298
+
299
+ module.exports = { cmdEvolve, countPendingGrowthProposals, listGrowthProposals };
@@ -97,22 +97,20 @@ const SEMVER_RE = /^v?[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/;
97
97
  const ENV_RE = /^[A-Z][A-Z0-9_]*$/;
98
98
  const SAFE_IDEMPOTENCY_RE = /^[A-Za-z0-9._:-]{8,200}$/;
99
99
 
100
- const SECRET_PATTERNS = [
101
- /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/i,
102
- /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/i,
103
- /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
104
- /\bAKIA[0-9A-Z]{16}\b/,
105
- /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i,
106
- /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|passwd|private[_-]?key|cookie)\b\s*[:=]\s*['"]?[^\s'"]{8,}/i,
107
- /\bauthorization\b\s*[:=]\s*['"]?(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/i,
108
- ];
100
+ const { SECRET_PATTERNS } = require("./agentlas-secret-patterns.cjs");
109
101
  const PII_PATTERNS = [
110
102
  /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
111
103
  /(?<!\w)(?:\+?\d[\d ().-]{8,}\d)(?!\w)/,
112
104
  /\b(?:account|customer|client|tenant|workspace|user)[ _-]?(?:id|key|number|no)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}\b|(?:계정|고객|사용자)[ _-]?(?:id|아이디|번호)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}/i,
113
105
  ];
106
+ // Absolute LOCAL paths and file URLs only. The previous alternation matched any
107
+ // slash-containing token, so ordinary prose lost its experience: "TCP/IP", "read/write",
108
+ // "and/or", and web routes like "GET /api/users" were all reported as a local path. A
109
+ // leading-slash path now has to look like a real filesystem root (or start from a home /
110
+ // relative marker, a Windows drive, or a UNC share); a lone `/word` — which is what a web
111
+ // route looks like — no longer counts, and neither does `word/word` inside a sentence.
114
112
  const LOCAL_PATH_PATTERNS = [
115
- /(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(?!\/|\s)(?:[^/\s"'`<>]+\/)*[^/\s"'`<>]+|[A-Za-z]:[/\\]\S+|\\\\[^\\/\s]+[\\/][^\\/\s]+))/i,
113
+ /(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(?:Users|home|root|private|var|tmp|opt|etc|srv|mnt|media|Volumes|Applications|System|Library|usr)\/[^\s"'`<>]+|[A-Za-z]:[/\\]\S+|\\\\[^\\/\s]+[\\/][^\\/\s]+))/i,
116
114
  ];
117
115
  const RAW_INTERACTION_PATTERNS = [
118
116
  /(?:^|\n)\s*(?:system|assistant|user|tool|customer|agent)\s*:\s+/i,
@@ -1530,7 +1528,13 @@ function isCanonicalTaskId(value) {
1530
1528
  function keywordOccurs(normalizedPrompt, rawKeyword) {
1531
1529
  const keyword = normalizeClassificationText(rawKeyword);
1532
1530
  if (!keyword) return false;
1533
- if (/[가-힣]/.test(keyword)) return normalizedPrompt.includes(keyword);
1531
+ if (/[가-힣]/.test(keyword)) {
1532
+ // Korean has no word boundary, so a raw includes() matched inside longer compounds:
1533
+ // 번역 hit 번역기, 금융 hit compound finance words, 영업 hit 영업일 (business day).
1534
+ // Require the keyword not be glued to another Hangul syllable on either side.
1535
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1536
+ return new RegExp(`(?<![가-힣])${escaped}(?![가-힣])`).test(normalizedPrompt);
1537
+ }
1534
1538
  return ` ${normalizedPrompt} `.includes(` ${keyword} `);
1535
1539
  }
1536
1540
 
@@ -1557,6 +1561,56 @@ function deriveCanonicalTaskClasses(prompt, options = {}) {
1557
1561
  return { taskIds: matches, source: "deterministic-keyword-map", matchedTaskClasses: matches, invalidDeclaredCount: 0 };
1558
1562
  }
1559
1563
 
1564
+ /**
1565
+ * Meaning-aware task classification. The resident judge decides; TASK_CLASS_KEYWORDS is
1566
+ * passed as reference hints only. This is what a keyword map cannot do: Korean compounds
1567
+ * (번역 inside 번역기), particle inflection (고객 문의를), dialect, slang, and any other
1568
+ * language all hinge on meaning, and no list enumerates them.
1569
+ *
1570
+ * An explicit declared task class still wins outright (it is data, not a guess), and with
1571
+ * no connected model the deterministic keyword prefilter is the fallback so classification
1572
+ * never stops working.
1573
+ */
1574
+ async function resolveCanonicalTaskClasses(prompt, options = {}) {
1575
+ const declaredRaw = options.declaredTaskClasses ?? options.declaredTaskClass;
1576
+ if (declaredRaw != null && (Array.isArray(declaredRaw) ? declaredRaw.length : String(declaredRaw).trim())) {
1577
+ return deriveCanonicalTaskClasses(prompt, options);
1578
+ }
1579
+ const prefilter = deriveCanonicalTaskClasses(prompt, options);
1580
+ const judgment = require("./agentlas-judgment.cjs");
1581
+ if (!judgment.hasJudgmentRunner()) return prefilter;
1582
+
1583
+ const hints = {};
1584
+ for (const slug of CANONICAL_TASK_SLUGS) {
1585
+ const words = TASK_CLASS_KEYWORDS[slug];
1586
+ if (Array.isArray(words) && words.length) hints[slug] = words;
1587
+ }
1588
+ const verdict = await judgment.judgeLabels({
1589
+ kind: "experience-task-class",
1590
+ question:
1591
+ "Which kinds of work does this request actually involve? Judge the user's real task, not words that merely appear.",
1592
+ labels: CANONICAL_TASK_SLUGS,
1593
+ input: String(prompt || ""),
1594
+ guidance:
1595
+ "Return a label only when that kind of work is genuinely part of the request. A word inside an " +
1596
+ "unrelated compound or a different sense of the word does not count. Return an empty list for " +
1597
+ "content with no identifiable task (hashes, ids, random strings).",
1598
+ hints,
1599
+ fallback: [],
1600
+ signal: options.signal,
1601
+ });
1602
+ if (verdict.source !== "llm") return prefilter;
1603
+ const taskIds = CANONICAL_TASK_IDS.filter((id) =>
1604
+ verdict.labels.some((slug) => id === `${CANONICAL_TASK_PREFIX}${slug}`));
1605
+ return {
1606
+ taskIds,
1607
+ source: "model-judgment",
1608
+ matchedTaskClasses: taskIds,
1609
+ invalidDeclaredCount: 0,
1610
+ ...(verdict.reason ? { judgmentReason: verdict.reason } : {}),
1611
+ };
1612
+ }
1613
+
1560
1614
  function parseEnvironmentConstraint(value) {
1561
1615
  const normalized = normalizedTaxonomyAtom(value);
1562
1616
  const contract = EXPERIENCE_TAXONOMY_V1.environment;
@@ -2140,6 +2194,7 @@ module.exports = {
2140
2194
  environmentConstraintsMatch,
2141
2195
  selectApplicablePortableItems,
2142
2196
  deriveCanonicalTaskClasses,
2197
+ resolveCanonicalTaskClasses,
2143
2198
  readExactLocalBaseMarker,
2144
2199
  exactTaskSignatureInPrompt,
2145
2200
  resolveRuntimeExperienceForAgent,
@@ -111,6 +111,8 @@ const STRINGS = {
111
111
  "team.usage": "usage: /team · /team <agent> <claude-code|codex|gemini|auto>",
112
112
  "team.set": "%s → %s",
113
113
  "routedImage": "routed to %s for image support",
114
+ "judge.source.llm": "judged by the connected model",
115
+ "judge.source.fallback": "deterministic fallback (no connected-model verdict)",
114
116
  "usageBar": "tokens",
115
117
  "config.title": "Engine auto engagement — explicit on/off (default: off)",
116
118
  "config.storm": "Stormbreaker auto-engage on direct-routed real work",
@@ -325,6 +327,8 @@ const STRINGS = {
325
327
  "team.usage": "사용법: /team · /team <에이전트> <claude-code|codex|gemini|auto>",
326
328
  "team.set": "%s → %s",
327
329
  "routedImage": "이미지 지원을 위해 %s로 라우팅",
330
+ "judge.source.llm": "판정: 연결 모델",
331
+ "judge.source.fallback": "판정: 결정적 폴백(연결 모델 없음)",
328
332
  "usageBar": "토큰",
329
333
  "config.title": "엔진 자동 개입 — 명시적 on/off (기본: off)",
330
334
  "config.storm": "직답 라우팅된 실작업에 Stormbreaker 자동 개입",
Binary file
@@ -30,17 +30,7 @@ const FINAL_SCOPES = new Set(["user_global", "team", "agent", "project", "sessio
30
30
  const SEMANTIC_DISPOSITIONS = new Set(["retain", "session", "discard", "review"]);
31
31
  const CONFIDENCE_LEVELS = new Set(["high", "medium", "low"]);
32
32
 
33
- const SECRET_PATTERNS = [
34
- /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/i,
35
- /\b(?:sk|rk|pk)-(?:ant|proj|live|test)?-?[A-Za-z0-9_-]{16,}\b/i,
36
- /\bgh[pousr]_[A-Za-z0-9]{20,}\b/i,
37
- /\bxox[baprs]-[A-Za-z0-9-]{16,}\b/i,
38
- /\bAIza[A-Za-z0-9_-]{30,}\b/,
39
- /\bAKIA[A-Z0-9]{16}\b/,
40
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
41
- /\b(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*[^\s,;]{6,}/i,
42
- /\bauthorization\s*:\s*bearer\s+[^\s,;]{8,}/i,
43
- ];
33
+ const { SECRET_PATTERNS } = require("./agentlas-secret-patterns.cjs");
44
34
  const ABSOLUTE_PATH_PATTERNS = [
45
35
  /(?:^|[\s("'`])~\/[A-Za-z0-9._-]/,
46
36
  /(?:^|[\s("'`])\/(?:Users|home|private|var|tmp|opt|etc|Volumes|Applications|System|Library)\//,