agentlas 1.0.47 → 1.0.49

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 (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +4 -2
  3. package/bin/agentlas.cjs +10 -0
  4. package/engine/acp/server.cjs +279 -0
  5. package/engine/agentlas-capabilities.cjs +2 -1
  6. package/engine/agentlas-input.cjs +2 -1
  7. package/engine/agentlas-native-host.cjs +6 -0
  8. package/engine/agentlas-onboard.cjs +2 -1
  9. package/engine/agentlas-sqlite-policy.cjs +9 -0
  10. package/engine/automation/daemon.cjs +3 -7
  11. package/engine/bootstrap-schema.sql +1037 -1010
  12. package/engine/cloud-assets/package.cjs +11 -3
  13. package/engine/cloud-assets/upload-scan-catalog.generated.cjs +102 -0
  14. package/engine/commands/acp.cjs +45 -0
  15. package/engine/commands/billing.cjs +2 -2
  16. package/engine/commands/call.cjs +4 -0
  17. package/engine/commands/doctor.cjs +2 -1
  18. package/engine/commands/index.cjs +2 -0
  19. package/engine/commands/workforce.cjs +11 -0
  20. package/engine/core/db.cjs +53 -1
  21. package/engine/core/desktop-core.cjs +108 -4
  22. package/engine/core/store-schema.cjs +119 -0
  23. package/engine/firms/orchestrate.cjs +32 -1
  24. package/engine/project/memory-context.cjs +7 -0
  25. package/engine/runtimes/acp-driver.cjs +96 -0
  26. package/engine/runtimes/detect.cjs +3 -13
  27. package/engine/runtimes/kinds.cjs +84 -0
  28. package/engine/runtimes/resolve.cjs +40 -7
  29. package/engine/ui/commands-catalog.cjs +2 -0
  30. package/engine/ui/palette.cjs +2 -1
  31. package/engine/ui/repl.cjs +2 -1
  32. package/engine/ui/shell.cjs +38 -3
  33. package/engine/vendor/desktop-core.manifest.json +5 -5
  34. package/engine/workforce/capture.cjs +4 -8
  35. package/engine/workforce/deps.cjs +7 -0
  36. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - `agentlas acp` — Agentlas as an Agent Client Protocol agent (Phase B-3). Zed,
6
+ JetBrains IDEs and other ACP clients can run the project controller (or an
7
+ installed agent via `_meta.agentlas.agent`) on the runtime you subscribe to,
8
+ with tool calls and streamed text projected onto ACP. `--info` prints the
9
+ registry-style descriptor. CLI-only (stdout is the wire).
10
+ - kimi, grok, and cursor run through the desktop core's generic ACP runner
11
+ (`engine/runtimes/acp-driver.cjs`) instead of being refused as "no v2
12
+ streaming driver". Same file, same tool-call vocabulary as Desktop; an old
13
+ core without `electron/runtime/acp.js` still refuses honestly with a repair
14
+ hint. `agentlas doctor` lists the newly executable runtimes.
15
+ - `npm test` is the smoke gate; the stale `.internal` copy of the architecture
16
+ sync script is gone (one canonical script under `scripts/`).
17
+
18
+
3
19
  ## 1.0.47 — 2026-08-14
4
20
 
5
21
  Antigravity and the shared runtime contracts now reach the independent
package/README.md CHANGED
@@ -228,7 +228,9 @@ The launcher (`bin/agentlas.cjs`) runs system Node against `engine/`. The defaul
228
228
  | Windows | `%APPDATA%\Agentlas` |
229
229
  | Linux | `$XDG_CONFIG_HOME/Agentlas` (default `~/.config/Agentlas`) |
230
230
 
231
- 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.
231
+ The SQLite database file is `agentlas.sqlite` (`user_version=97`). 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.
232
+
233
+ **Single migration authority.** The Desktop app owns the schema migration ladder; the CLI never migrates the shared database. `engine/bootstrap-schema.sql` is generated by running that ladder to completion against an empty database, so a CLI-created store already sits at the ladder head and has nothing left for Desktop to upgrade. If the CLI opens a store older than the version it knows, it refuses with an actionable message instead of migrating or silently proceeding — a second migrator on this lock-free file is what corrupted the store once before. On a machine with no Desktop app, an operator can set `AGENTLAS_STORE_MIGRATION_ROLE=owner` for a single deliberate upgrade run with every other Agentlas process closed.
232
234
 
233
235
  SQLite driver priority: `better-sqlite3` (optional dependency native build), then Node 22+ `node:sqlite` when the first driver is unavailable.
234
236
 
@@ -289,7 +291,7 @@ Engine source code resides in `engine/*.cjs`.
289
291
  ```sh
290
292
  sh test/smoke.sh # Runs surface tests, guard tests, fresh DB tests, contract tests & parity gates
291
293
  npm run smoke # Equivalent to npm run test:release-contracts
292
- sh scripts/gen-bootstrap-schema.sh [db-path] # Regenerates engine/bootstrap-schema.sql
294
+ node scripts/gen-bootstrap-schema.cjs # Regenerates engine/bootstrap-schema.sql from the Desktop ladder (never reads a live store)
293
295
  ```
294
296
 
295
297
  Smoke tests run isolated inside a temporary `AGENTLAS_USER_DATA_DIR` and do not touch local user data.
package/bin/agentlas.cjs CHANGED
@@ -155,12 +155,22 @@ function bootstrapDbIfMissing() {
155
155
  throw new Error(`Bootstrap schema not found: ${schemaFile}`);
156
156
  }
157
157
  const sql = fs.readFileSync(schemaFile, "utf8");
158
+ // ★새 저장소는 태어날 때부터 WAL 이어야 한다 (2026-08-18 실측).
159
+ // journal_mode 는 파일에 박히는 값이고, delete→WAL 전환은 **배타 락**을 요구하며
160
+ // busy_timeout 이 걸려 있어도 즉시 SQLITE_BUSY 로 실패할 수 있다. 그래서 터미널이
161
+ // 만든 DB 를 delete 모드로 남기면, 데스크탑과 터미널이 그 파일을 처음 동시에 여는
162
+ // 순간 한쪽이 "database is locked" 로 죽는다 — 첫 부팅에서만 나는, 재현이 어려운
163
+ // 결함이다. 여기서 한 번 켜 두면 그 전환 자체가 존재하지 않는다.
164
+ const WAL_PRAGMA = "PRAGMA journal_mode=WAL;\n";
158
165
  // DB를 정식 경로에서 직접 만들면 두 첫 실행이 모두 exists=false를 본 뒤 한 프로세스의
159
166
  // 실패 cleanup이 다른 프로세스의 정상 DB까지 지울 수 있다. 각자 같은 볼륨의 임시 DB를
160
167
  // 완성하고 hard-link(EEXIST=다른 프로세스 승리)로만 정식 이름을 원자 획득한다.
161
168
  const temp = `${p}.bootstrap-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
162
169
  const db = openSqlite(temp);
163
170
  try {
171
+ // 임시 파일에서 켠다. journal_mode 는 DB 헤더에 박히므로 hard-link 로 정식 이름을
172
+ // 얻은 뒤에도 유지된다(닫을 때 -wal 은 체크포인트되고 사라진다).
173
+ db.exec(WAL_PRAGMA);
164
174
  db.exec(sql);
165
175
  } catch (e) {
166
176
  db.close();
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /*
3
+ * acp/server — Agentlas as an ACP *agent* (PRD 2026-08-15 Phase B-3, plan-acp §2 (B)).
4
+ *
5
+ * `agentlas acp` speaks the Agent Client Protocol v1 on stdio (JSON-RPC 2.0, ndjson)
6
+ * as the AGENT side. Any ACP client — Zed, JetBrains IDEs, VS Code extensions,
7
+ * Emacs/neovim, other Agentlas surfaces — can now run Agentlas' project controller
8
+ * (the agent Desktop/Terminal already run for `agentlas run`) on the runtime the
9
+ * user subscribes to (Claude Code, Codex, Antigravity, …), inside their editor.
10
+ *
11
+ * BYOM stays true: Agentlas has no model of its own. A prompt turn is exactly an
12
+ * `agentlas run` turn (engine/sessions/session.cjs → native-host / ACP driver),
13
+ * projected onto ACP notifications:
14
+ * stream-delta → agent_message_chunk · tool → tool_call(completed) ·
15
+ * tool-result → tool_call_update · status → (dropped) · error → stopReason/refusal
16
+ * Nothing here executes anything a plain `agentlas run` would not; the same
17
+ * permission model (prefs.permission, default read for a remote client) applies.
18
+ *
19
+ * Wire choices, all measured against the registry matrix behaviour:
20
+ * protocolVersion 1 only (v2 is a draft) · authMethods [] (Agentlas login is a
21
+ * separate `agentlas login`; nothing to authenticate over ACP) ·
22
+ * session/cancel kills the child · unknown methods → -32601.
23
+ */
24
+ const readline = require("node:readline");
25
+ const path = require("node:path");
26
+
27
+ const PROTOCOL_VERSION = 1;
28
+
29
+ function pkgVersion() {
30
+ try { return require(path.join(__dirname, "..", "..", "package.json")).version || "0.0.0"; } catch { return "0.0.0"; }
31
+ }
32
+
33
+ function textOfPrompt(blocks) {
34
+ if (!Array.isArray(blocks)) return String(blocks || "");
35
+ return blocks.map((b) => {
36
+ if (!b || typeof b !== "object") return "";
37
+ if (b.type === "text") return String(b.text || "");
38
+ if (b.type === "resource" && b.resource && typeof b.resource.text === "string") return `\n[resource ${b.resource.uri || ""}]\n${b.resource.text}\n`;
39
+ if (b.type === "resource_link") return `[${b.name || "resource"}](${b.uri || ""})`;
40
+ return "";
41
+ }).join("");
42
+ }
43
+
44
+ /**
45
+ * Turn-execution boundary. Production uses engine/sessions (Orchestrator + project
46
+ * controller); contract tests inject a fake `runTurn` so no runtime is spawned.
47
+ *
48
+ * runTurn(ctx, {cwd, prompt, permission, runtimeKind?, sessionKey?, events}) →
49
+ * Promise<{ text, error, errorKind, session }>
50
+ * events: { onDelta(text), onTool(name, summary, id), onToolResult(text, ok, id), onStatus(text) }
51
+ * cancel(sessionKey) → void
52
+ */
53
+ function productionTurnRunner() {
54
+ const sessions = new Map(); // acp sessionId → { session, agent, runtime }
55
+ return {
56
+ async newSession(ctx, { cwd, runtimeKind, agentSlug }) {
57
+ const { projectCwd } = require("../project/paths.cjs");
58
+ const { resolveProjectController, withProjectControllerContext } = require("../project/controller.cjs");
59
+ const { resolveRuntimeForAgent } = require("../runtimes/overrides.cjs");
60
+ const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
61
+ const { findAgent } = require("../agents/registry.cjs");
62
+ const permissions = require("../agentlas-permissions.cjs");
63
+ const db = ctx.db();
64
+ const workdir = cwd || projectCwd();
65
+ // Same ladder as `agentlas run [agent]`: an explicit installed agent
66
+ // (session/new _meta.agentlas.agent) is the advanced direct call; otherwise
67
+ // the folder's project controller — and no silent substitution when neither.
68
+ let agent = null;
69
+ let resolved = null;
70
+ if (agentSlug) {
71
+ agent = findAgent(db, String(agentSlug));
72
+ if (!agent) throw new Error(`unknown agent: ${agentSlug} (agentlas agents lists installed agents)`);
73
+ } else {
74
+ resolved = resolveProjectController(db, workdir);
75
+ agent = withProjectControllerContext(resolved.controller, resolved.project);
76
+ }
77
+ const runtime = resolveRuntimeForAgent({ db, prefs: ctx.prefs, explicit: runtimeKind || null, role: "orchestrator", agentId: agent.id });
78
+ // A remote editor client is unattended: never widen beyond the saved preference, default read.
79
+ const permission = permissions.normalize((ctx.prefs && ctx.prefs.permission) || "read");
80
+ ensureTerminalProjectForExecutionCli(db, workdir, permission, "terminal-acp");
81
+ return { agent, runtime, permission, cwd: workdir, project: resolved ? resolved.project : null };
82
+ },
83
+ async runTurn(ctx, spec, prompt, events) {
84
+ const { Orchestrator } = require("../sessions/orchestrator.cjs");
85
+ const orch = new Orchestrator({ db: ctx.db(), lang: ctx.lang });
86
+ const session = orch.spawn({ agent: spec.agent, runtime: spec.runtime, permission: spec.permission, cwd: spec.cwd, title: prompt.slice(0, 60) });
87
+ sessions.set(spec.acpSessionId, session);
88
+ const listener = (ev) => {
89
+ try {
90
+ if (ev.type === "stream-delta") events.onDelta(ev.text);
91
+ else if (ev.type === "tool") events.onTool(ev.name, ev.summary, ev.id);
92
+ else if (ev.type === "tool-result") events.onToolResult(ev.text, ev.ok, ev.id);
93
+ else if (ev.type === "status") events.onStatus(ev.text);
94
+ } catch { /* a projection failure must not break the turn */ }
95
+ };
96
+ session.on("event", listener);
97
+ try {
98
+ const res = await session.send(prompt);
99
+ return {
100
+ text: (res && (res.finalText || res.text)) || "",
101
+ error: session.status === "failed" ? (session.lastError || (res && res.error) || "failed") : (res && res.error) || null,
102
+ errorKind: res && res.errorKind,
103
+ cancelled: session.status === "killed",
104
+ };
105
+ } finally {
106
+ session.removeListener("event", listener);
107
+ sessions.delete(spec.acpSessionId);
108
+ }
109
+ },
110
+ cancel(acpSessionId) {
111
+ const s = sessions.get(acpSessionId);
112
+ if (s) { try { s.kill(); } catch { /* best effort */ } }
113
+ },
114
+ };
115
+ }
116
+
117
+ class AcpAgentServer {
118
+ /**
119
+ * @param {object} opts { ctx, input?, output?, runner? }
120
+ * ctx: agentlas command ctx (db(), prefs, lang) — may be null in tests with a fake runner
121
+ */
122
+ constructor(opts) {
123
+ this.ctx = opts.ctx || null;
124
+ this.input = opts.input || process.stdin;
125
+ this.output = opts.output || process.stdout;
126
+ this.runner = opts.runner || productionTurnRunner();
127
+ this.sessions = new Map(); // sessionId → spec
128
+ this.initialized = false;
129
+ this.nextSessionSeq = 1;
130
+ this.closed = false;
131
+ }
132
+
133
+ send(obj) {
134
+ if (this.closed) return;
135
+ this.output.write(JSON.stringify(obj) + "\n");
136
+ }
137
+ notify(method, params) { this.send({ jsonrpc: "2.0", method, params }); }
138
+ reply(id, result) { this.send({ jsonrpc: "2.0", id, result }); }
139
+ error(id, code, message, data) { this.send({ jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined ? { data } : {}) } }); }
140
+
141
+ start() {
142
+ const rl = readline.createInterface({ input: this.input, crlfDelay: Infinity });
143
+ rl.on("line", (line) => {
144
+ const trimmed = line.trim();
145
+ if (!trimmed) return;
146
+ let msg;
147
+ try { msg = JSON.parse(trimmed); } catch { return; }
148
+ if (!msg || typeof msg !== "object" || !msg.method) return;
149
+ void this.handle(msg);
150
+ });
151
+ rl.on("close", () => { this.closed = true; });
152
+ return new Promise((resolve) => rl.on("close", resolve));
153
+ }
154
+
155
+ async handle(msg) {
156
+ const { id, method } = msg;
157
+ const params = msg.params || {};
158
+ try {
159
+ switch (method) {
160
+ case "initialize": {
161
+ const requested = Number(params.protocolVersion);
162
+ if (requested !== PROTOCOL_VERSION) {
163
+ return this.error(id, -32602, `unsupported protocolVersion ${params.protocolVersion}; this agent speaks v${PROTOCOL_VERSION}`);
164
+ }
165
+ this.initialized = true;
166
+ return this.reply(id, {
167
+ protocolVersion: PROTOCOL_VERSION,
168
+ agentCapabilities: {
169
+ loadSession: false,
170
+ promptCapabilities: { image: false, audio: false, embeddedContext: true },
171
+ mcpCapabilities: { http: false, sse: false },
172
+ },
173
+ authMethods: [],
174
+ agentInfo: { name: "agentlas", title: "Agentlas", version: pkgVersion() },
175
+ });
176
+ }
177
+ case "authenticate":
178
+ return this.reply(id, {});
179
+ case "session/new": {
180
+ if (!this.initialized) return this.error(id, -32002, "initialize first");
181
+ const sessionId = `agentlas-${process.pid}-${this.nextSessionSeq++}`;
182
+ const meta = (params._meta && params._meta.agentlas) || params._meta || {};
183
+ const spec = await this.runner.newSession(this.ctx, { cwd: params.cwd, runtimeKind: meta.runtime, agentSlug: meta.agent });
184
+ spec.acpSessionId = sessionId;
185
+ this.sessions.set(sessionId, spec);
186
+ const rt = spec.runtime || {};
187
+ return this.reply(id, {
188
+ sessionId,
189
+ configOptions: [
190
+ {
191
+ id: "runtime", category: "mode", type: "select", name: "Runtime",
192
+ currentValue: rt.kind || "",
193
+ options: [{ value: rt.kind || "", name: `${rt.kind || "runtime"}${rt.model ? ` · ${rt.model}` : ""}` }],
194
+ },
195
+ ...(rt.model ? [{ id: "model", category: "model", type: "select", name: "Model", currentValue: rt.model, options: [{ value: rt.model, name: rt.model }] }] : []),
196
+ ],
197
+ _meta: {
198
+ agentlas: {
199
+ controller: spec.agent && (spec.agent.slug || spec.agent.name),
200
+ project: spec.project && spec.project.name,
201
+ permission: spec.permission,
202
+ runtime: rt.kind,
203
+ },
204
+ },
205
+ });
206
+ }
207
+ case "session/prompt": {
208
+ const spec = this.sessions.get(String(params.sessionId));
209
+ if (!spec) return this.error(id, -32602, "unknown sessionId");
210
+ const prompt = textOfPrompt(params.prompt).trim();
211
+ if (!prompt) return this.reply(id, { stopReason: "end_turn" });
212
+ const sid = spec.acpSessionId;
213
+ let streamed = false;
214
+ const events = {
215
+ onDelta: (text) => {
216
+ if (!text) return;
217
+ streamed = true;
218
+ this.notify("session/update", { sessionId: sid, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } } });
219
+ },
220
+ onTool: (name, summary, toolId) => {
221
+ this.notify("session/update", { sessionId: sid, update: {
222
+ sessionUpdate: "tool_call", toolCallId: String(toolId || `${name}-${Date.now()}`), title: [name, summary].filter(Boolean).join(" "),
223
+ kind: kindOfTool(name), status: "completed",
224
+ } });
225
+ },
226
+ onToolResult: (text, ok, toolId) => {
227
+ if (!toolId) return;
228
+ this.notify("session/update", { sessionId: sid, update: {
229
+ sessionUpdate: "tool_call_update", toolCallId: String(toolId), status: ok === false ? "failed" : "completed",
230
+ content: text ? [{ type: "content", content: { type: "text", text: String(text).slice(0, 4000) } }] : undefined,
231
+ } });
232
+ },
233
+ onStatus: () => {},
234
+ };
235
+ const res = await this.runner.runTurn(this.ctx, spec, prompt, events);
236
+ if (res && res.cancelled) return this.reply(id, { stopReason: "cancelled" });
237
+ if (res && res.error) {
238
+ const kind = String(res.errorKind || "");
239
+ if (kind === "refused") return this.reply(id, { stopReason: "refusal" });
240
+ if (kind === "auth") return this.error(id, -32000, `auth_required: ${res.error}`);
241
+ // Non-marker failures: surface the runtime's own words as the answer, then end the turn.
242
+ if (!streamed) events.onDelta(String(res.error));
243
+ return this.reply(id, { stopReason: "end_turn", _meta: { agentlas: { error: String(res.error), errorKind: kind || null } } });
244
+ }
245
+ if (!streamed && res && res.text) events.onDelta(res.text);
246
+ return this.reply(id, { stopReason: "end_turn" });
247
+ }
248
+ case "session/cancel": {
249
+ const spec = this.sessions.get(String(params.sessionId));
250
+ if (spec) this.runner.cancel(spec.acpSessionId);
251
+ return; // notification: no reply
252
+ }
253
+ case "session/set_mode":
254
+ case "session/set_model":
255
+ return this.error(id, -32601, `Method not found: ${method}`);
256
+ default:
257
+ if (id !== undefined) return this.error(id, -32601, `Method not found: ${method}`);
258
+ return;
259
+ }
260
+ } catch (e) {
261
+ if (id !== undefined) this.error(id, -32603, (e && e.message) || String(e));
262
+ }
263
+ }
264
+ }
265
+
266
+ function kindOfTool(name) {
267
+ const n = String(name || "").toLowerCase();
268
+ if (/^(bash|shell|exec|command|run)/.test(n)) return "execute";
269
+ if (/read|cat|view|open/.test(n)) return "read";
270
+ if (/edit|write|patch|apply|create/.test(n)) return "edit";
271
+ if (/delete|remove|rm/.test(n)) return "delete";
272
+ if (/move|rename/.test(n)) return "move";
273
+ if (/grep|search|find|glob/.test(n)) return "search";
274
+ if (/fetch|http|web|browse/.test(n)) return "fetch";
275
+ if (/think|plan/.test(n)) return "think";
276
+ return "other";
277
+ }
278
+
279
+ module.exports = { AcpAgentServer, PROTOCOL_VERSION, textOfPrompt, kindOfTool, productionTurnRunner };
@@ -26,7 +26,8 @@ const RUNTIME_CAPS = {
26
26
 
27
27
  // NOTE: grok은 CAPS(멀티모달 능력 인지)만 등록 — 터미널 스폰 러너(RUNTIME_BIN)가 아직 없어
28
28
  // CLI_KINDS에 넣으면 repl의 which(RUNTIME_BIN[k]) 탐지가 깨진다. 러너 추가 시 함께 확장할 것.
29
- const CLI_KINDS = ["claude-code", "codex", "agy", "gemini"];
29
+ // 목록의 정본은 runtimes/kinds.cjs 여기서는 네이티브 스폰 러너 4종만 가져다 쓴다.
30
+ const CLI_KINDS = require("./runtimes/kinds.cjs").NATIVE_CLI_KINDS;
30
31
 
31
32
  function capsFor(spec) {
32
33
  return RUNTIME_CAPS[spec] || { code: true, image: false, label: spec || "?" };
@@ -306,7 +306,8 @@ const SLASH_COMMAND_META = [
306
306
  { command: "/exit", description: "Quit Agentlas", category: "Session", usage: "/exit", detail: "Close the terminal session.", aliases: ["/quit"] },
307
307
  ];
308
308
  const SLASH_COMMANDS = SLASH_COMMAND_META.flatMap((entry) => [entry.command].concat(entry.aliases || []));
309
- const RUNTIME_SPECS = ["claude-code", "codex", "agy", "gemini", "anthropic", "openai", "google", "ollama", "upstage"];
309
+ // /runtime 받는 spec 전체(네이티브 CLI + API 백엔드) 정본은 runtimes/kinds.cjs.
310
+ const RUNTIME_SPECS = require("./runtimes/kinds.cjs").RUNTIME_SPECS;
310
311
  const PERM_LEVELS = ["read", "write", "full"];
311
312
 
312
313
  const HELP_KEY_BY_COMMAND = {
@@ -19,6 +19,7 @@ const fs = require("node:fs");
19
19
  const os = require("node:os");
20
20
  const path = require("node:path");
21
21
  const permissions = require("./agentlas-permissions.cjs");
22
+ const acpDriver = require("./runtimes/acp-driver.cjs");
22
23
  const i18n = require("./agentlas-i18n.cjs");
23
24
  const { wrapStdioServer } = require("./agentlas-mcp-env.cjs");
24
25
 
@@ -915,6 +916,11 @@ function handleGeminiLine(line, st, ui) {
915
916
  function runNativeTurn(req) {
916
917
  const { kind, bin, ui } = req;
917
918
  const cwd = req.cwd;
919
+ // kimi/grok/cursor: 손코딩 3번째 대신 벤더 코어의 공용 ACP 러너로 (PRD 2026-08-15 T-2).
920
+ // 같은 결과 계약({text, session, usage, error, errorKind, errorSource})으로 돌아온다.
921
+ if (acpDriver.ACP_KINDS.has(kind)) {
922
+ return acpDriver.runAcpTurn(req);
923
+ }
918
924
  let launchReq = req;
919
925
  if (
920
926
  kind === "gemini" && permissions.normalize(req.permission) === "full" &&
@@ -133,7 +133,8 @@ async function runOnboard({ ui, rl, helpers, persist }) {
133
133
  // Step 2 — default runtime
134
134
  ui.line("");
135
135
  printIndented(ui.t("wiz.runtimeQ"), c.bold);
136
- const cliKinds = ["claude-code", "codex", "agy", "gemini"];
136
+ // 위저드 선택지 = 네이티브 스폰 러너 4종 (정본 runtimes/kinds.cjs, 표시 순서 포함).
137
+ const cliKinds = require("./runtimes/kinds.cjs").NATIVE_CLI_KINDS;
137
138
  const rtOpts = [{ value: "auto", label: ui.t("wiz.runtimeAuto") }];
138
139
  for (const k of cliKinds) {
139
140
  const has = !!H.which(H.RUNTIME_BIN[k]);
@@ -4,6 +4,15 @@
4
4
  // lock waits bounded, and acquire writer authority before a transaction reads
5
5
  // state that it may subsequently update. A deferred read-to-write upgrade can
6
6
  // fail immediately with SQLITE_BUSY even when busy_timeout is configured.
7
+ //
8
+ // ★This value must stay identical to `STORE_BUSY_TIMEOUT_MS` in
9
+ // agentlas_desktop/electron/store/db.ts. Until 2026-08-18 the Desktop waited 5s
10
+ // and the terminal 15s on the very same file, so under contention the Desktop
11
+ // was always the first to give up with SQLITE_BUSY — even when the terminal was
12
+ // the slow writer. That asymmetry made shared-file contention look like a
13
+ // Desktop-only bug. 15s is the agreed value: nothing holds a transaction on this
14
+ // file for long (the longest writer is the migration ladder), so it is a ceiling
15
+ // that is essentially never reached rather than added latency.
7
16
  const SQLITE_BUSY_TIMEOUT_MS = 15_000;
8
17
 
9
18
  // `foreign_keys` 는 파일이 아니라 **커넥션** 속성이다. 데스크탑은
@@ -24,13 +24,9 @@ const store = require("./store.cjs");
24
24
  // getAutomationExecutionContractState 동형) ──────────────────────────────
25
25
  // 손상된/미래 계약 값은 절대 조용히 넓혀 실행하지 않는다 — raw-row 게이트로
26
26
  // 무인 실행 직전에 검사한다(데스크탑 automation-scheduler.ts:538-549).
27
- const RUNTIME_KINDS = new Set([
28
- "claude-code", "codex", "agy", "gemini", "kimi", "grok", "cursor", "byok", "ollama", "lmstudio", "mlx",
29
- ]);
30
- const RUNTIME_BACKENDS = new Set([
31
- "anthropic", "openai", "google", "ollama", "lmstudio", "mlx", "upstage", "custom", "glm",
32
- "kimi", "deepseek", "minimax", "xai", "openrouter", "cursor",
33
- ]);
27
+ const { CONTRACT_RUNTIME_KINDS, CONTRACT_RUNTIME_BACKENDS } = require("../runtimes/kinds.cjs");
28
+ const RUNTIME_KINDS = new Set(CONTRACT_RUNTIME_KINDS);
29
+ const RUNTIME_BACKENDS = new Set(CONTRACT_RUNTIME_BACKENDS);
34
30
  const RUNTIME_SELECTION_KEYS = new Set(["kind", "backend", "source", "model", "longContext", "effort"]);
35
31
 
36
32
  function decodeRuntimeSelection(raw) {