agentlas 1.0.22 → 1.0.23

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.23 — 2026-08-01
4
+
5
+ - **Semantic routing stays with the connected model.** Image-capability and
6
+ installed-agent routing no longer retain regex hints, keyword glossaries,
7
+ deterministic role vetoes, or caller-authored fallback labels. If the model
8
+ cannot return a valid judgment, the decision remains unavailable.
9
+ - **Fresh Terminal databases match Desktop schema 86.** The retired
10
+ chat-level hired-agent roster is absent from the bootstrap schema; project
11
+ order and current-turn task-force targets own controller and helper binding.
12
+
3
13
  ## 1.0.22 — 2026-08-01
4
14
 
5
15
  - **Task classes no longer come from a fixed keyword dictionary.** An explicit
package/README.md CHANGED
@@ -54,33 +54,21 @@ agentlas
54
54
  agentlas "Identify why repository tests are failing" # Uses the connected project's first agent as controller
55
55
  agentlas run -p "Summarize the last 3 CHANGELOG entries" # Prints the project controller's final answer to stdout
56
56
  git log --oneline -20 | agentlas run # Reads stdin; still requires a connected project
57
- agentlas run <agent> "Advanced direct request" # Exact-agent direct invocation; no project substitution
58
- agentlas <agent> # Starts an interactive REPL with an exact agent
57
+ agentlas run <agent> "Advanced direct request" # Explicit one-turn exact-agent override
59
58
  agentlas firm <firm> "Request" # Delegates to an exact firm CEO
60
59
  ```
61
60
 
62
61
  Ordinary one-shot work is project-first: run it inside a folder connected to a Desktop Work project. The first agent in that project's ordered pool owns the task; remaining members are eligible task-scoped sub-agents. Missing projects, empty teams, and unavailable controllers stop without substituting another agent. The separate `agentlas route` preview is model-judged and stays unresolved when no exact installed agent can be justified.
63
62
 
64
- **3. Resuming & Managing Conversations**
65
-
66
- ```sh
67
- agentlas chats # List recent conversations (shares SQLite DB with Desktop app)
68
- agentlas chats 30 # List last 30 conversations
69
- agentlas open <chat-id> # Re-open REPL with the chat's assigned agent
70
- ```
71
-
72
63
  ## Command Reference
73
64
 
74
65
  Run `agentlas help` for the authoritative command list. Below is the organized command structure:
75
66
 
76
- ### TALK & RUN
67
+ ### PROJECT WORK
77
68
  ```sh
78
- agentlas <agent> # Chat with agent (equivalent to chat <agent>)
79
- agentlas chat <agent>
80
- agentlas run [agent] [prompt] # Single-run execution (-p, --runtime, --permission, stdin)
69
+ agentlas # Open the connected project's controller session
70
+ agentlas run [agent] [prompt] # Project-first run; optional agent is one explicit turn
81
71
  agentlas firm <firm> [task] # Delegate task to firm CEO
82
- agentlas chats [n] # List recent chat conversations
83
- agentlas open <chat-id> # Resume conversation by ID
84
72
  agentlas cd <agent> # Print folder path of an agent (cd "$(agentlas cd x)")
85
73
  ```
86
74
 
@@ -164,7 +152,7 @@ $ agentlas lst
164
152
  Command list: agentlas help · To run this exact prompt: agentlas run -p "lst"
165
153
  ```
166
154
 
167
- Desktop-only surface names (`site`, `trex`, `prompts`, `dashboard`, `marketplace`, `library`, `groups`, `settings`, `apps`, `quests`, `bookmarks`, `one`) also halt with typo guard guidance directing you to equivalent terminal commands. To run a single word as a prompt, wrap it in quotes or pass `run -p`.
155
+ Desktop-only surface names (`site`, `trex`, `prompts`, `dashboard`, `marketplace`, `library`, `settings`, `apps`, `quests`, `bookmarks`, `one`) also halt with typo guard guidance directing you to equivalent terminal commands. To run a single word as a prompt, wrap it in quotes or pass `run -p`.
168
156
 
169
157
  ### Permission Levels
170
158
 
@@ -176,9 +164,9 @@ Desktop-only surface names (`site`, `trex`, `prompts`, `dashboard`, `marketplace
176
164
 
177
165
  *Note: Saved `full` permission is session-bound and automatically fails closed to `write` on subsequent executions. REPL shell execution (`!<command>`) runs strictly under `full` permission, enforced with 8MB output capping, secret masking, and process-group termination.*
178
166
 
179
- ## REPL & Orca Multi-Session Architecture
167
+ ## Project Work Execution Tree
180
168
 
181
- Executing `agentlas` with no arguments starts an interactive REPL session. Foreground execution turns run inside an active session, while subagents created via `/spawn` execute concurrently in background sessions. The terminal streams output from the currently focused active session and displays one-line notices when background tasks finish.
169
+ Executing `agentlas` with no arguments resolves the current folder to an Agentlas project and starts its first ordered agent as the task controller. The controller may create task-scoped worker runs; users can inspect the real execution tree, but cannot replace the controller or address workers around it.
182
170
 
183
171
  ### REPL Slash Commands
184
172
 
@@ -187,14 +175,9 @@ Executing `agentlas` with no arguments starts an interactive REPL session. Foreg
187
175
  | `/help` | Display available commands and keybindings |
188
176
  | `/sessions` · `/tree` | View session table or parent-child process tree |
189
177
  | `/s <n>` · `/switch <n>` | Switch active session (replays tail logs & subscribes to live stream) |
190
- | `/spawn <agent> [task]` | Spawn subagent background session (executes task immediately if supplied) |
191
- | `/steer <n> <msg>` | Queue instruction for session `n`'s next turn |
192
178
  | `/kill <n>` | Interrupt active turn in session `n` |
193
179
  | `/rm <n>` | Remove session `n` |
194
- | `/broadcast <msg>` | Broadcast instruction to all running sessions |
195
- | `/use <agent>` | Change active agent in main session |
196
180
  | `/agents` · `/list` | List installed agents |
197
- | `/chats [n]` | View recent conversations |
198
181
  | `/mcp` | List active MCP servers |
199
182
  | `/doctor` | Run runtime & database diagnostics |
200
183
  | `/runtime <kind>` | Set runtime for new sessions (`claude-code` \| `codex` \| `gemini`) |
@@ -242,9 +225,9 @@ The launcher (`bin/agentlas.cjs`) runs system Node against `engine/`. The defaul
242
225
  | Windows | `%APPDATA%\Agentlas` |
243
226
  | Linux | `$XDG_CONFIG_HOME/Agentlas` (default `~/.config/Agentlas`) |
244
227
 
245
- The SQLite database file is `agentlas.sqlite` (`user_version=85`). When launched for the first time without an existing database, it bootstraps schemas using `engine/bootstrap-schema.sql`. Consequently, **projects, installed agents, task history, automation sessions, and MCP registrations are shared across Desktop and Terminal**. Subagent sessions spawned via `/spawn` are stored as scoped execution ledgers rather than new global Work conversations.
228
+ The SQLite database file is `agentlas.sqlite` (`user_version=86`). When launched for the first time without an existing database, it bootstraps schemas using `engine/bootstrap-schema.sql`. Consequently, **projects, installed agents, task history, automation sessions, and MCP registrations are shared across Desktop and Terminal**. The first ordered project agent remains the controller; additional agents are task-scoped and are stored only as execution ledgers, never as global conversations or durable owners.
246
229
 
247
- SQLite Driver Fallback Ladder: `better-sqlite3` (optionalDependency native build) -> Node 22+ `node:sqlite`.
230
+ SQLite driver priority: `better-sqlite3` (optional dependency native build), then Node 22+ `node:sqlite` when the first driver is unavailable.
248
231
 
249
232
  ### Hub Installation & Safety Policy
250
233
 
@@ -195,7 +195,6 @@ function renderBanner(ctx) {
195
195
  const menu = [
196
196
  [ui.t("banner.menu.help"), "/help"],
197
197
  [ui.t("banner.menu.sessions"), "/sessions"],
198
- [ui.t("banner.menu.chats"), "/chats"],
199
198
  [ui.t("banner.menu.quit"), "/quit"],
200
199
  ];
201
200
  const infoRoom = inner - CARD_PAD * 2;
@@ -40,58 +40,9 @@ function runtimeFromSpec(spec) {
40
40
  return CLI_KINDS.includes(spec) ? { mode: "cli", kind: spec } : { mode: "api", backend: spec, model: null };
41
41
  }
42
42
 
43
- // Does this agent's job involve generating/handling images?
44
- // /그림(?!자)/ "그림자"(shadow)는 이미지 힌트가 아니다.
45
- const IMAGE_HINTS = [
46
- /image/i, /이미지/, /그림(?!자)/, /\bdesign\b/i, /디자인/, /쇼핑몰/, /상품\s*(사진|이미지|상세)/, /상세\s*페이지/,
47
- /thumbnail/i, /썸네일/, /banner/i, /배너/, /poster/i, /포스터/, /visual/i, /비주얼/, /illustrat/i, /일러스트/,
48
- /로고/, /\blogo\b/i, /사진/, /photo/i, /nano-?banana/i, /imagen/i, /이미지\s*생성/, /그래픽/, /graphic/i,
49
- ];
50
- // 빌더/메타/조율/거버넌스 역할은 (이미지 에이전트를 *만들* 수는 있어도) 스스로 이미지를 생산하지 않는다.
51
- // 이런 역할이 system_prompt에 "이미지/디자인"을 언급한다는 이유로 gemini로 끌려가면 코드/빌드 품질이 떨어진다.
52
- const NON_IMAGE_ROLES = new Set(["meta", "builder", "orchestrator", "pm", "curator", "governance"]);
53
- // 부정/거절 문장("이미지 생성 금지", "영상·이미지 생성은 하지 않는다")은 능력이 아니라
54
- // 반(反)능력 선언이다 — 그 문장 안의 힌트는 세지 않는다. 단, "묻지 않고 바로 생성한다"처럼
55
- // 긍정문에 흔한 보조 부정("않고","없다" 단독)은 잡지 않도록 강한 금지 구문만 매칭한다.
56
- const IMAGE_NEGATION_RE = /(금지|하지\s*않|하지\s*마|말\s*것|거절|불가(?!피)|아니다|refuse|\bnever\b|\bdo(es)?\s+not\b|\bdon'?t\b)/i;
57
- // 이미지 생성 도구 이름은 단독으로도 이미지 생산 역할의 강한 증거다.
58
- const IMAGE_TOOL_MARKERS = [/nano-?banana/i, /\bimagen\b/i, /gpt-image/i, /grok\s*imagine/i];
59
- // 사고(2026-07-12): appbridge CEO 프롬프트의 "코드/디자인/스토어 결정의 owner가 아니다" 속
60
- // "디자인" 한 단어로 이미지 에이전트 판정 → PPT 요청 세션이 통째로 gemini로 전환됐다.
61
- // 수리: 정체성 존(이름/태그라인)은 그대로 신뢰하되, 본문 단독으로는 "힌트를 포함한 긍정문
62
- // 3문장 이상"을 요구한다. 문장 단위로 세므로 "상품 이미지 생성 금지" 같은 한 문장이
63
- // 겹치는 정규식 여러 개를 동시에 때려도 1클러스터다.
64
- const MIN_BODY_IMAGE_SENTENCES = 3;
65
- const BODY_SCAN_CAP = 16000; // 로컬 임포트 상한과 동일 — 클라우드 무제한 프롬프트의 전문 스캔 방지
66
- // 어휘 스코어 — IMAGE_HINTS 단어목록에 대한 참고용 스코어러. 하우스 룰(2026-07-25):
67
- // 단어목록은 최종 결정을 내리지 않는다. resolveNeedsImage가 IMAGE_HINTS를 힌트로만 모델에
68
- // 넘기고, 연결 모델이 없으면 이미지 여부를 판정하지 않는다(어휘 폴백 제거). 이 함수는 힌트
69
- // 품질을 고정하는 참조 스코어러로만 남는다(회귀 테스트가 정밀도를 검증).
70
- function needsImageLexical(agent) {
71
- if (!agent) return false;
72
- if (NON_IMAGE_ROLES.has(String(agent.role || "").toLowerCase())) return false;
73
- // 정체성 존은 사용자가 선언한 이름/태그라인만 — slug는 폴더명에서 기계 파생되므로
74
- // ("design-system" 리포 임포트 등) 단독 신뢰 대상이 아니다.
75
- const identity = [agent.name, agent.name_en, agent.tagline, agent.tagline_en].filter(Boolean).join(" ");
76
- if (IMAGE_HINTS.some((re) => re.test(identity))) return true;
77
- // 팀 CEO 두뇌(부서 소개·위임 규칙)에서는 body 키워드가 역할 증거가 아니다 — vibecoder처럼
78
- // "Design and Publishing HQ" 부서명이 10문장씩 나오는 조율용 프롬프트가 이미지 팀으로
79
- // 오판되던 사례. entity_kind='team'은 사용자가 선언한 정체성 존만 신뢰한다.
80
- if (String(agent.entity_kind || "").toLowerCase() === "team") return false;
81
- const body = String(agent.system_prompt || "").slice(0, BODY_SCAN_CAP);
82
- let clusters = 0;
83
- for (const sentence of body.split(/[\n.!?。!?]+/)) {
84
- if (!sentence || IMAGE_NEGATION_RE.test(sentence)) continue;
85
- if (IMAGE_TOOL_MARKERS.some((re) => re.test(sentence))) return true;
86
- if (IMAGE_HINTS.some((re) => re.test(sentence))) {
87
- clusters += 1;
88
- if (clusters >= MIN_BODY_IMAGE_SENTENCES) return true;
89
- }
90
- }
91
- return false;
92
- }
93
-
94
- // ── 상주 판정 서비스 배선 — 모델이 의미로 최종 판정, IMAGE_HINTS는 참고 힌트 ──────
43
+ // Image capability is judged only by the connected model from the complete
44
+ // agent identity and instructions. No regex, keyword glossary, role veto, or
45
+ // default runtime may make this semantic decision.
95
46
  // needsImage 호출자(REPL 배지·autoRuntimeFor·routingNote)는 동기라서 warm-cache 패턴:
96
47
  // 비동기 경로(resolveNeedsImage)가 먼저 판정해 캐시를 데우고, 동기 needsImage는 캐시만
97
48
  // 읽는다. 캐시 미스 = "이미지 아님"으로 처리(어휘 폴백 없음) — 모델 판정만 이미지로 인정한다.
@@ -101,22 +52,13 @@ function imageJudgeInput(agent) {
101
52
  const identity = [agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].filter(Boolean).join(" | ");
102
53
  return `${identity}\n---\n${String(agent.system_prompt || "").slice(0, 6000)}`;
103
54
  }
104
- // 역할/팀 베토는 보수적 하드 가드로 유지 — 조율 두뇌가 부서 소개 문장("Design HQ")으로
105
- // 이미지 팀이 되던 사고(vibecoder/appbridge)는 모델 판정 대상에서 아예 뺀다.
106
- function imageJudgeVetoed(agent) {
107
- if (!agent) return true;
108
- if (NON_IMAGE_ROLES.has(String(agent.role || "").toLowerCase())) return true;
109
- if (String(agent.entity_kind || "").toLowerCase() === "team") return true;
110
- return false;
111
- }
112
55
  // 이 에이전트의 직무가 이미지 생산인지를 연결 모델이 의미로 판정한다.
113
56
  // 하우스 룰(2026-07-25): 연결 모델이 없으면 단어목록으로 이미지 여부를 결정하지 않는다.
114
57
  // 러너 없음/타임아웃/정크 → source:"unavailable"(판정 불가)로 반환하고, 호출자는 안전
115
58
  // 기본값(세션 런타임 유지, gemini/codex 하이재킹 금지)을 지킨다. 이미지 판정은 저위험
116
59
  // 능력 추론이라 실패-닫힘이 아니라 "판정 안 함"으로 정직하게 둔다.
117
60
  async function resolveNeedsImage(agent) {
118
- // 팀/역할 하드 가드는 구조적 판단(키워드 아님) 판정 대상에서 제외하고 결정적으로 not-image.
119
- if (imageJudgeVetoed(agent)) return { image: false, source: "deterministic" };
61
+ if (!agent) return { image: false, source: "unavailable", decided: false };
120
62
  let judgment;
121
63
  try {
122
64
  judgment = require("./agentlas-judgment.cjs");
@@ -134,13 +76,11 @@ async function resolveNeedsImage(agent) {
134
76
  labels: ["image", "not-image"],
135
77
  multi: false,
136
78
  input,
137
- hints: { image: IMAGE_HINTS.map((re) => re.source) },
138
79
  guidance:
139
80
  "Judge the agent's role from its identity and instructions, in any language. Mentioning images is not " +
140
81
  "producing them: builders, orchestrators, PMs, curators, and coordination brains that commission or " +
141
82
  "delegate image work are 'not-image'. Refusals or prohibitions ('never generate images') declare the " +
142
83
  "opposite of a capability.",
143
- fallback: [],
144
84
  });
145
85
  // 모델이 판정을 못 냈다 → 어휘 폴백 없이 "판정 불가". 캐시하지 않아 이후 모델 연결 시 재판정한다.
146
86
  if (verdict.source !== "llm" || !verdict.labels.length) return { image: false, source: "unavailable", decided: false };
@@ -155,18 +95,16 @@ async function resolveNeedsImage(agent) {
155
95
  // Does this agent's job involve generating/handling images? Sync surface for badges and
156
96
  // autoRuntimeFor. House rule (2026-07-25): only a warm MODEL verdict counts as "image".
157
97
  // A cache miss (no model judgment / not warmed) is treated as "not specifically an image
158
- // agent" so a keyword guess can never hijack the runtime to gemini/codex. The lexical
159
- // scorer (needsImageLexical) survives only as the hint source for the judge, not a decision.
98
+ // agent" so an unproven semantic guess can never hijack the runtime to gemini/codex.
160
99
  function needsImage(agent) {
161
100
  if (!agent) return false;
162
- if (imageJudgeVetoed(agent)) return false;
163
101
  const cached = imageVerdicts.get(imageJudgeInput(agent));
164
102
  return cached ? cached.image : false;
165
103
  }
166
- // 라벨용 — 에이전트의 현재 이미지 판정이 모델("llm")인지 결정적 경로("deterministic")인지.
104
+ // 라벨용 — 모델 판정이 없으면 unavailable이며 다른 의미 판정으로 대체하지 않는다.
167
105
  function imageJudgmentSource(agent) {
168
- if (agent && !imageJudgeVetoed(agent) && imageVerdicts.has(imageJudgeInput(agent))) return "llm";
169
- return "deterministic";
106
+ if (agent && imageVerdicts.has(imageJudgeInput(agent))) return "llm";
107
+ return "unavailable";
170
108
  }
171
109
  function clearImageJudgments() {
172
110
  imageVerdicts.clear();
@@ -195,7 +133,6 @@ module.exports = {
195
133
  specOf,
196
134
  runtimeFromSpec,
197
135
  needsImage,
198
- needsImageLexical,
199
136
  resolveNeedsImage,
200
137
  imageJudgmentSource,
201
138
  clearImageJudgments,
@@ -40,6 +40,8 @@ function runtimeRoots(explicitRoot) {
40
40
  binRoot,
41
41
  path.join(os.homedir(), ".agentlas", "runtime", "current"),
42
42
  ];
43
+ // Monorepo/development parity with Desktop's signed-bundle candidate.
44
+ roots.push(path.resolve(__dirname, "..", "..", "agentlas_desktop", "Hephaestus"));
43
45
  if (process.resourcesPath) roots.push(path.join(process.resourcesPath, "Hephaestus"));
44
46
  if (process.platform === "darwin") roots.push("/Applications/Agentlas.app/Contents/Resources/Hephaestus");
45
47
  return unique(roots);
@@ -77,13 +79,12 @@ function resolveCoreRuntimeRootFromCandidates(candidateRoots, requiredMarkers =
77
79
  const markers = [...CORE_RUNTIME_MARKERS, ...requiredMarkers];
78
80
  const minVersion = options.minVersion ? normalizeSemVer(String(options.minVersion)) : null;
79
81
  if (options.minVersion && !minVersion) throw new Error(`Invalid minimum Core version: ${options.minVersion}`);
82
+ const eligible = [];
80
83
  for (const root of unique(candidateRoots)) {
81
84
  try {
82
85
  if (!markers.every((segments) => fs.existsSync(path.join(root, ...segments)))) continue;
83
- if (minVersion) {
84
- const version = readCoreRuntimeVersion(root);
85
- if (!version || compareSemVer(version, minVersion) < 0) continue;
86
- }
86
+ const version = readCoreRuntimeVersion(root);
87
+ if (minVersion && (!version || compareSemVer(version, minVersion) < 0)) continue;
87
88
  // `~/.agentlas/runtime/current` 는 업데이터가 원자적으로 갈아 끼우는 심볼릭
88
89
  // 링크다. 그 경로를 그대로 넘기면 Python 이 import 를 늦게 해석하는 특성상,
89
90
  // 긴 실행 도중 업데이트가 일어나면 **옛 버전 모듈과 새 버전 모듈이 한 프로세스에
@@ -92,17 +93,23 @@ function resolveCoreRuntimeRootFromCandidates(candidateRoots, requiredMarkers =
92
93
  //
93
94
  // 실경로로 고정해도 라이브 버전 선택은 그대로다: **다음** 호출이 다시 해석해
94
95
  // 새 릴리스를 집는다. 없어지는 것은 실행 중 교체뿐이다.
95
- try {
96
- return fs.realpathSync(root);
97
- } catch {
98
- // 링크를 읽을 수 없다고 실행을 거부할 이유는 없다 — 이전 동작대로 경로를 쓴다.
99
- return root;
100
- }
96
+ let pinned = root;
97
+ try { pinned = fs.realpathSync(root); } catch {}
98
+ eligible.push({ root: pinned, version });
101
99
  } catch {
102
100
  // Continue to the next installed/bundled root.
103
101
  }
104
102
  }
105
- return null;
103
+ // Desktop and Terminal must attach to the same newest valid Core. Candidate
104
+ // order is only a tie-breaker; an older managed runtime may never shadow a
105
+ // newer signed Desktop bundle after an app update.
106
+ eligible.sort((left, right) => {
107
+ if (!left.version && !right.version) return 0;
108
+ if (!left.version) return 1;
109
+ if (!right.version) return -1;
110
+ return compareSemVer(right.version, left.version);
111
+ });
112
+ return eligible[0]?.root ?? null;
106
113
  }
107
114
 
108
115
  function resolveCoreRuntimeRoot(explicitRoot, requiredMarkers = [], options = {}) {
@@ -21,7 +21,7 @@ const STRINGS = {
21
21
  "banner.location": "%s · / commands · Shift-Tab permissions",
22
22
  "banner.menu.help": "Command reference",
23
23
  "banner.menu.sessions": "Running sessions",
24
- "banner.menu.chats": "Past conversations",
24
+ "banner.menu.chats": "Project runs",
25
25
  "banner.menu.quit": "Quit",
26
26
  "status.title": "Session",
27
27
  "status.runtime": "runtime",
@@ -93,10 +93,10 @@ const STRINGS = {
93
93
  "permFullConfirm": "FULL ACCESS ON FOR THIS SESSION — approvals and sandboxing are bypassed",
94
94
  "sideUsage": "usage: /side <question>",
95
95
  "sideNeedsSubject": "pick an agent first, or type the task normally so Agentlas can auto-route it",
96
- "sideStart": "side question; this answer will not be saved to chat context",
97
- "sideDone": "side question complete; main chat context unchanged",
98
- "sideCancelled": "side question cancelled; main chat context unchanged",
99
- "sideFailed": "side question failed; main chat context unchanged",
96
+ "sideStart": "side question; this answer will not be saved to the project task",
97
+ "sideDone": "side question complete; project task context unchanged",
98
+ "sideCancelled": "side question cancelled; project task context unchanged",
99
+ "sideFailed": "side question ended; project task context unchanged",
100
100
  "noAgent": "no agent: %s",
101
101
  "noCompany": "no company: %s",
102
102
  "agentUsage": "usage: /agent <name>",
@@ -139,7 +139,7 @@ const STRINGS = {
139
139
  "team.set": "%s → %s",
140
140
  "routedImage": "routed to %s for image support",
141
141
  "judge.source.llm": "judged by the connected model",
142
- "judge.source.fallback": "deterministic fallback (no connected-model verdict)",
142
+ "judge.source.fallback": "unresolved (no model judgment)",
143
143
  "usageBar": "session usage",
144
144
  "config.title": "Engine auto engagement — explicit on/off (default: off)",
145
145
  "config.storm": "Stormbreaker auto-engage on direct-routed real work",
@@ -150,7 +150,7 @@ const STRINGS = {
150
150
  "config.autoEngage": "auto-engaging %s (opted in via /config — turn off with /config %s off)",
151
151
  "help.config": "explicit on/off for Stormbreaker/Workforce Ontology auto engagement",
152
152
  "guard.imageWarn": "this agent works with images, but %s can't generate them — try /runtime gemini (or codex), or /team to pin one",
153
- "multimodal.title": "Multimodal fallback",
153
+ "multimodal.title": "Multimodal provider priority",
154
154
  "multimodal.set": "multimodal %s → %s",
155
155
  "multimodal.usage": "set with /multimodal set <image|video|audio> <provider-id>",
156
156
  // help rows
@@ -177,12 +177,12 @@ const STRINGS = {
177
177
  "help.memory": "show the memory being injected",
178
178
  "help.careerGraph": "show/add source refs for the Career Graph routing index",
179
179
  "help.ontology": "turn on/list/add project ontology sources with short natural commands",
180
- "help.side": "ask a side question without saving it to chat context",
180
+ "help.side": "ask a side question without saving it to project task context",
181
181
  "help.status": "show model/runtime, agent, permission, and directory",
182
- "help.multimodal": "show/set image, video, and audio fallback providers",
182
+ "help.multimodal": "show/set image, video, and audio provider priorities",
183
183
  "help.import": "import a local folder (agent or team)",
184
184
  "help.market": "browse/install marketplace agents",
185
- "help.clear": "clear the chat and redraw",
185
+ "help.clear": "clear the terminal view and redraw",
186
186
  "help.storm": "run a force-robust Stormbreaker pipeline on a goal",
187
187
  "help.build": "build/repair/package an agent or team (Hephaestus)",
188
188
  "help.route": "preview which agent/pipeline would take a request",
@@ -272,7 +272,7 @@ const STRINGS = {
272
272
  "banner.location": "%s · / 명령 · Shift-Tab 권한",
273
273
  "banner.menu.help": "명령 보기",
274
274
  "banner.menu.sessions": "실행 중인 세션",
275
- "banner.menu.chats": "지난 대화",
275
+ "banner.menu.chats": "프로젝트 실행",
276
276
  "banner.menu.quit": "종료",
277
277
  "status.title": "세션",
278
278
  "status.runtime": "런타임",
@@ -342,10 +342,10 @@ const STRINGS = {
342
342
  "permFullConfirm": "이 세션에서만 무제한 권한 켜짐 — 승인과 샌드박스를 우회함",
343
343
  "sideUsage": "사용법: /side <질문>",
344
344
  "sideNeedsSubject": "먼저 에이전트를 고르거나, 그냥 할 일을 입력해서 Agentlas가 자동 라우팅하게 하세요.",
345
- "sideStart": "사이드 질문입니다. 이 답변은 메인 대화 맥락에 저장하지 않습니다.",
346
- "sideDone": "사이드 질문 완료. 메인 대화 맥락은 그대로입니다.",
347
- "sideCancelled": "사이드 질문이 중단되었습니다. 메인 대화 맥락은 그대로입니다.",
348
- "sideFailed": "사이드 질문이 실패했습니다. 메인 대화 맥락은 그대로입니다.",
345
+ "sideStart": "사이드 질문입니다. 이 답변은 프로젝트 작업에 저장하지 않습니다.",
346
+ "sideDone": "사이드 질문 완료. 프로젝트 작업 맥락은 그대로입니다.",
347
+ "sideCancelled": "사이드 질문이 중단되었습니다. 프로젝트 작업 맥락은 그대로입니다.",
348
+ "sideFailed": "사이드 질문이 종료되었습니다. 프로젝트 작업 맥락은 그대로입니다.",
349
349
  "noAgent": "에이전트 없음: %s",
350
350
  "noCompany": "회사 없음: %s",
351
351
  "agentUsage": "사용법: /agent <이름>",
@@ -388,7 +388,7 @@ const STRINGS = {
388
388
  "team.set": "%s → %s",
389
389
  "routedImage": "이미지 지원을 위해 %s로 라우팅",
390
390
  "judge.source.llm": "판정: 연결 모델",
391
- "judge.source.fallback": "판정: 결정적 폴백(연결 모델 없음)",
391
+ "judge.source.fallback": "판정 미확정(모델 판단 없음)",
392
392
  "usageBar": "세션 사용량",
393
393
  "config.title": "엔진 자동 개입 — 명시적 on/off (기본: off)",
394
394
  "config.storm": "직답 라우팅된 실작업에 Stormbreaker 자동 개입",
@@ -399,7 +399,7 @@ const STRINGS = {
399
399
  "config.autoEngage": "%s 자동 개입 (/config로 켜져 있음 — 해제: /config %s off)",
400
400
  "help.config": "Stormbreaker/Workforce Ontology 자동 개입 명시적 on/off",
401
401
  "guard.imageWarn": "이 에이전트는 이미지 작업인데 %s는 이미지 생성 불가 — /runtime gemini(또는 codex), 또는 /team으로 고정",
402
- "multimodal.title": "멀티모달 대체 제공자",
402
+ "multimodal.title": "멀티모달 제공자 우선순위",
403
403
  "multimodal.set": "멀티모달 %s → %s",
404
404
  "multimodal.usage": "/multimodal set <image|video|audio> <provider-id> 로 변경",
405
405
  "help.talk": "현재 에이전트/회사와 대화 — 스트리밍 + 툴",
@@ -425,12 +425,12 @@ const STRINGS = {
425
425
  "help.memory": "주입되는 메모리 보기",
426
426
  "help.careerGraph": "경력 그래프 라우팅 색인의 원본 경로 보기/등록",
427
427
  "help.ontology": "짧은 자연어로 프로젝트 온톨로지 켜기/목록/자료 등록",
428
- "help.side": "메인 대화 맥락에 저장하지 않는 사이드 질문",
428
+ "help.side": "프로젝트 작업 맥락에 저장하지 않는 사이드 질문",
429
429
  "help.status": "모델/런타임, 에이전트, 권한, 작업 폴더 보기",
430
- "help.multimodal": "이미지·영상·음성 대체 제공자 보기/변경",
430
+ "help.multimodal": "이미지·영상·음성 제공자 우선순위 보기/변경",
431
431
  "help.import": "로컬 폴더(에이전트/팀) 임포트",
432
432
  "help.market": "마켓플레이스 에이전트 보기/설치",
433
- "help.clear": "대화 비우고 다시 그리기",
433
+ "help.clear": "터미널 화면 비우고 다시 그리기",
434
434
  "help.storm": "목표를 Stormbreaker 견고 파이프라인으로 실행",
435
435
  "help.build": "에이전트/팀 빌드·수리·패키징 (Hephaestus)",
436
436
  "help.route": "요청이 어떤 에이전트로 라우팅되는지 미리보기",
@@ -443,7 +443,7 @@ const STRINGS = {
443
443
  "help.connect": "Telegram/플랫폼을 에이전트 팀에 연결",
444
444
  "help.swarm": "목표에 창발형 에이전트 스웜 전개",
445
445
  "help.doctor": "런타임/데이터 점검",
446
- "help.compact": "오래된 대화 맥락을 줄이고 최근 맥락 유지",
446
+ "help.compact": "오래된 작업 맥락을 줄이고 최근 맥락 유지",
447
447
  "help.keybindings": "터미널 단축키 보기",
448
448
  "help.exit": "종료 (Ctrl-C: 턴 중단 / 유휴 시 종료)",
449
449
  "help.talkKey": "(메시지 입력)",
Binary file
@@ -964,7 +964,6 @@ function runNativeTurn(req) {
964
964
  return;
965
965
  }
966
966
  ui.stopSpinner();
967
- ui.error(uiText(ui, "runtime.failed", kind, err.message));
968
967
  finish({ text: "", session: st.session, error: err.message });
969
968
  });
970
969
  child.on("close", (code) => {
@@ -976,39 +975,13 @@ function runNativeTurn(req) {
976
975
  ui.streamEnd();
977
976
  ui.stopSpinner();
978
977
  const text = (st.finalText || st.text || "").trim();
979
- const errTail = stripAnsi(stderrBuf).replace(/\s+/g, " ").trim(); // ANSI 제거 + 한 줄로
980
- if (st.error && !st.errorShown) {
981
- // claude `result` is_error 등 — 이전에 표시되지 않은 에러를 노출
982
- ui.error(String(st.error));
983
- } else if (code !== 0) {
984
- // Runtime Doctor — 아는 시스템 원인(미인증 OAuth MCP 플러그인 등)이면 즉시 수리하고
985
- // 1회 자동 재시도한다(2026-07-08 notion@openai-curated가 codex 전멸시킨 사고).
986
- if (!req._doctorRetried) {
987
- try {
988
- const { runRuntimeDoctor } = require("./agentlas-doctor.cjs");
989
- const report = runRuntimeDoctor(`${kind} exited with code ${code}\n${stripAnsi(stderrBuf)}`);
990
- if (report.repaired) {
991
- ui.warn(uiText(ui, "runtime.doctor", report.summary));
992
- for (const act of report.actions) ui.warn(` ${act.title} — ${act.detail}`);
993
- ui.warn(uiText(ui, "runtime.doctorRetry"));
994
- settled = true;
995
- cleanup();
996
- resolve(runNativeTurn({ ...req, _doctorRetried: true }));
997
- return;
998
- }
999
- } catch {
1000
- /* 닥터 실패는 원래 에러 표출을 막지 않는다 */
1001
- }
1002
- }
1003
- ui.error(uiText(ui, "runtime.exited", kind, String(code)) + (errTail ? `\n ${errTail.slice(-400)}` : ""));
1004
- } else if (!text && !st.error) {
1005
- // 정상 종료인데 출력이 비어 있음(거부/차단 등) — 무음 실패 방지
1006
- ui.warn(uiText(ui, "runtime.noOutput", kind) + (errTail ? ` (${errTail.slice(-200)})` : ""));
1007
- }
1008
978
  if (st.usage) ui.cost(st.usage);
1009
979
  const exitError = code === 0
1010
980
  ? st.error
1011
- : st.error || `${kind} exited with code ${code == null ? "unknown" : code}`;
981
+ : st.error || [
982
+ `${kind} exited with code ${code == null ? "unknown" : code}`,
983
+ stripAnsi(stderrBuf).slice(-4000),
984
+ ].filter(Boolean).join("\n");
1012
985
  finish({ text, session: st.session, usage: st.usage, error: exitError });
1013
986
  });
1014
987
 
@@ -594,7 +594,19 @@ class Ui {
594
594
  }
595
595
  error(msg) {
596
596
  this.stopSpinner();
597
- this._message("✗ ", this.c.paw, this.c.text, msg);
597
+ // Last-resort presentation boundary for legacy/direct commands. Raw
598
+ // provider text, stack messages, paths and codes must never become UI.
599
+ // REPL/session paths route the private evidence to the controller before
600
+ // reaching this boundary; direct commands get a neutral recovery state.
601
+ void msg;
602
+ this._message(
603
+ "◆ ",
604
+ this.c.amber,
605
+ this.c.text,
606
+ this.lang === "ko"
607
+ ? "One이 상태를 확인하고 복구하고 있습니다."
608
+ : "One is checking and recovering this operation.",
609
+ );
598
610
  }
599
611
 
600
612
  // 최종 텍스트(비스트리밍 경로)에 가벼운 마크다운 강조 적용 후 출력.
@@ -308,7 +308,7 @@ function resolveAllocation(options = {}) {
308
308
  model: modelPin,
309
309
  effort: pinnedEffort,
310
310
  provider,
311
- source: modelPin || effortPinPresent ? "user-pin" : "fallback",
311
+ source: modelPin ? "user-pin" : "unresolved",
312
312
  fallbackReason: pinReasons.join(","),
313
313
  aiReason: null,
314
314
  };
@@ -355,24 +355,14 @@ function resolveAllocation(options = {}) {
355
355
 
356
356
  let model = selected && selected.id;
357
357
  let effectiveModelEntry = selected;
358
- let source = decision.exactModelId ? "parent-ai-exact" : "fallback";
358
+ let source = decision.exactModelId ? "parent-ai-exact" : "unresolved";
359
359
  if (modelPin) {
360
360
  model = modelPin;
361
361
  effectiveModelEntry = available.find((item) => item.id === modelPin) || null;
362
362
  source = "user-pin";
363
363
  reasons.push("explicit_model_pin");
364
364
  } else if (!model) {
365
- source = "fallback";
366
- const activeId = cleanText(runtime && runtime.model, 160) || null;
367
- const active = activeId ? available.find((item) => item.id === activeId) || null : null;
368
- const issue = activeId ? candidateIssue(active, "active_model") : "active_model_unavailable";
369
- if (issue) {
370
- if (!reasons.includes(issue)) reasons.push(issue);
371
- } else {
372
- model = active.id;
373
- effectiveModelEntry = active;
374
- reasons.push("compliant_active_model_fallback");
375
- }
365
+ source = "unresolved";
376
366
  }
377
367
 
378
368
  let effort;
@@ -406,21 +396,20 @@ function resolveAllocation(options = {}) {
406
396
  function resolveAllocationAcrossRuntimes(options = {}) {
407
397
  const runtimes = Array.isArray(options.runtimes) ? options.runtimes : [];
408
398
  const decision = normalizeAllocation(options.decision);
409
- const fallbackRuntime = options.fallbackRuntime || options.runtime || runtimes[0] || null;
410
- const fallbackId = cleanText(fallbackRuntime && (fallbackRuntime.runtimeId || fallbackRuntime.id), 255) || null;
399
+ const explicitRuntime = options.runtime || null;
411
400
  const requestedId = decision && decision.runtimeId;
412
401
  const chosen = requestedId
413
402
  ? runtimes.find((runtime, index) => (cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || `runtime-${index + 1}`) === requestedId) || null
414
- : fallbackRuntime;
415
- const runtime = chosen || fallbackRuntime;
403
+ : explicitRuntime;
404
+ const runtime = chosen || null;
416
405
  const resolution = resolveAllocation({ ...options, runtime, decision, availableModels: runtime && runtime.availableModels });
417
406
  const requestedExact = Boolean(decision && decision.runtimeId && decision.exactModelId);
418
- const runtimeId = cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || fallbackId;
407
+ const runtimeId = cleanText(runtime && (runtime.runtimeId || runtime.id), 255) || null;
419
408
  if (requestedExact && chosen && resolution.model === decision.exactModelId) {
420
409
  resolution.source = "parent-selected-live-runtime-model";
421
410
  } else if (requestedExact) {
422
411
  resolution.fallbackReason = [resolution.fallbackReason, chosen ? "parent_model_not_in_live_inventory" : "parent_runtime_not_in_live_inventory"].filter(Boolean).join(",");
423
- if (resolution.source !== "user-pin") resolution.source = "fallback";
412
+ if (resolution.source !== "user-pin") resolution.source = "unresolved";
424
413
  }
425
414
  return { ...resolution, runtime, runtimeId, requestedRuntimeId: requestedId || null };
426
415
  }
@@ -478,9 +467,7 @@ function createDecisionReceipt({ taskId, stage, decision, resolution, role, usag
478
467
  );
479
468
  const status = source === "user-pin" && hasResolvedCurrent
480
469
  ? "user-pin"
481
- : source === "fallback" && hasResolvedCurrent
482
- ? "fallback-current"
483
- : normalized && resolution && resolution.ok
470
+ : normalized && resolution && resolution.ok
484
471
  ? "resolved"
485
472
  : "unresolved";
486
473
  const riskCodes = new Set(normalized ? normalized.reasonCodes : []);
@@ -509,7 +496,7 @@ function createDecisionReceipt({ taskId, stage, decision, resolution, role, usag
509
496
  },
510
497
  reasonCodes,
511
498
  inputFeatureHash: normalized && normalized.inputFeatureHash ? normalized.inputFeatureHash : featureHash,
512
- selectorVersion: normalized ? normalized.selectorVersion : "deterministic-host-fallback",
499
+ selectorVersion: normalized ? normalized.selectorVersion : "unresolved-no-model-judgment",
513
500
  independentVerificationRequired:
514
501
  riskCodes.has("high-risk") || riskCodes.has("critical-risk") || riskCodes.has("independent-verification"),
515
502
  usage: normalizeObservedUsage(usage),
@@ -548,7 +535,7 @@ function plannerSystemPrompt({ language = "English", maxTasks = 12, mode = "swar
548
535
  return [
549
536
  `You are the higher-level workload allocator for an Agentlas ${mode}.`,
550
537
  "Judge each child task using the full goal and planned dependency graph. Do not use a keyword lookup or fixed role-to-model table.",
551
- "LIVE_RUNTIME_INVENTORY below is authoritative. For every child and synthesis, choose an exact runtimeId and exactModelId only from it. Do not infer, rename, or invent a model from a tier. If an exact choice cannot be justified, select the current-runtime fallback from the inventory.",
538
+ "LIVE_RUNTIME_INVENTORY below is authoritative. For every child and synthesis, choose an exact runtimeId and exactModelId only from it. Do not infer, rename, invent, or substitute a model. If an exact choice cannot be justified, return an unresolved decision.",
552
539
  `LIVE_RUNTIME_INVENTORY=${JSON.stringify(liveRuntimeInventory)}`,
553
540
  "Choose effort none|minimal|low|medium|high|xhigh|max. Spend frontier/high effort only when the task's complexity, risk, context, or synthesis burden justifies it.",
554
541
  `Return strict JSON only with at most ${Math.max(1, Math.min(24, maxTasks))} tasks:`,