agentlas 1.0.12 → 1.0.14
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 +39 -0
- package/README.md +4 -2
- package/bin/agentlas.cjs +17 -3
- package/engine/agentlas-config.cjs +25 -18
- package/engine/agentlas-core-harness.cjs +14 -1
- package/engine/agentlas-i18n.cjs +2 -0
- package/engine/agentlas-input.cjs +1 -1
- package/engine/agentlas-memory-governance.cjs +10 -0
- package/engine/agentlas-onboard.cjs +22 -5
- package/engine/agentlas-sqlite-policy.cjs +18 -7
- package/engine/agentlas-workforce.cjs +136 -19
- package/engine/agentlas-workload-routing.cjs +32 -4
- package/engine/agentlas.cjs +8 -0
- package/engine/automation/daemon.cjs +67 -30
- package/engine/automation/schedule.cjs +16 -0
- package/engine/automation/store.cjs +70 -12
- package/engine/bootstrap-schema.sql +788 -53
- package/engine/commands/automation.cjs +8 -0
- package/engine/commands/chats.cjs +6 -1
- package/engine/commands/firm.cjs +7 -0
- package/engine/commands/help.cjs +23 -2
- package/engine/commands/hep-cloud.cjs +31 -0
- package/engine/commands/hep-hub.cjs +30 -0
- package/engine/commands/hep-local.cjs +32 -0
- package/engine/commands/hep-network.cjs +43 -0
- package/engine/commands/index.cjs +38 -7
- package/engine/commands/open.cjs +5 -1
- package/engine/commands/run.cjs +12 -2
- package/engine/commands/setup.cjs +12 -11
- package/engine/commands/storm.cjs +5 -9
- package/engine/commands/swarm.cjs +4 -4
- package/engine/commands/uninstall.cjs +36 -2
- package/engine/commands/version.cjs +29 -0
- package/engine/commands/workforce.cjs +10 -10
- package/engine/core/schema-ensure.cjs +75 -0
- package/engine/experience/variant.cjs +46 -2
- package/engine/hephaestus/runtime.cjs +7 -0
- package/engine/memory-cli/curate.cjs +3 -7
- package/engine/project/memory-context.cjs +3 -7
- package/engine/project/state.cjs +7 -6
- package/engine/sessions/orchestrator.cjs +41 -3
- package/engine/sessions/prompt.cjs +4 -7
- package/engine/sessions/session.cjs +32 -0
- package/engine/storm/swarm.cjs +12 -0
- package/engine/ui/palette.cjs +14 -2
- package/engine/ui/renderer.cjs +37 -0
- package/engine/ui/repl.cjs +7 -1
- package/engine/workforce/concurrency.cjs +41 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* core/schema-ensure — 공유 SQLite 에 대한 스키마 보정을 **커넥션당 한 번만** 한다.
|
|
4
|
+
*
|
|
5
|
+
* 왜 (2026-07-28 실측):
|
|
6
|
+
* `agentlas.sqlite` 는 데스크탑 앱과 공유하는 파일이다(제품 계약, `core/paths.cjs`).
|
|
7
|
+
* 그런데 터미널이 **매 턴** 그 파일에 DDL 을 날리고 있었다 —
|
|
8
|
+
* · `ALTER TABLE memory_entries ADD COLUMN context_json` (사본 3벌)
|
|
9
|
+
* · `CREATE TABLE IF NOT EXISTS terminal_memory_*` 4개 + 인덱스 5개
|
|
10
|
+
* 전부 `IF NOT EXISTS`/조건부라 결과는 멱등이지만, **쓰기 락을 잡는 것은 매번**이다.
|
|
11
|
+
* 데스크탑이 마이그레이션 트랜잭션 중이면 15초 busy_timeout 을 소진하고, 호출부가
|
|
12
|
+
* 전부 빈 catch 라 그 실패가 조용히 사라진다.
|
|
13
|
+
*
|
|
14
|
+
* 스키마 소유자는 데스크탑이다(`core/db.cjs:7-9`). 터미널의 이 보정은 "앱보다 먼저
|
|
15
|
+
* 깔린 DB" 같은 경우를 위한 안전망이지 상시 작업이 아니다. 프로세스가 사는 동안
|
|
16
|
+
* 한 번이면 충분하다.
|
|
17
|
+
*
|
|
18
|
+
* 왜 커넥션당인가 (프로세스당이 아니라):
|
|
19
|
+
* 테스트와 일부 명령이 임시 DB 를 따로 연다. 프로세스 단위로 기억하면 두 번째
|
|
20
|
+
* DB 가 보정을 건너뛰어 조용히 깨진다. WeakMap 이면 커넥션이 사라질 때 같이 사라진다.
|
|
21
|
+
*/
|
|
22
|
+
const applied = new WeakMap();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `key` 작업을 이 커넥션에서 아직 안 했으면 실행한다.
|
|
26
|
+
* 실패는 삼키되 **기억하지 않는다** — 다음 호출이 다시 시도할 수 있어야 한다.
|
|
27
|
+
* (일시적 락 경합으로 실패한 것을 "완료"로 기억하면 스키마가 영구히 안 맞는다.)
|
|
28
|
+
*/
|
|
29
|
+
function ensureOnce(db, key, fn) {
|
|
30
|
+
if (!db) return false;
|
|
31
|
+
let done = applied.get(db);
|
|
32
|
+
if (!done) {
|
|
33
|
+
done = new Set();
|
|
34
|
+
applied.set(db, done);
|
|
35
|
+
}
|
|
36
|
+
if (done.has(key)) return true;
|
|
37
|
+
try {
|
|
38
|
+
fn(db);
|
|
39
|
+
done.add(key);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tableExists(db, name) {
|
|
47
|
+
try {
|
|
48
|
+
return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name));
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function columnExists(db, table, column) {
|
|
55
|
+
try {
|
|
56
|
+
return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `memory_entries.context_json` 보정. 이전에는 이 함수가 세 파일에 각각 복제돼 있었고
|
|
64
|
+
* (`sessions/prompt.cjs`, `project/memory-context.cjs`, `memory-cli/curate.cjs`)
|
|
65
|
+
* 그중 하나는 매 턴 호출됐다. 하나로 합치고 커넥션당 1회로 줄인다.
|
|
66
|
+
*/
|
|
67
|
+
function ensureMemoryContextColumn(db) {
|
|
68
|
+
return ensureOnce(db, "memory_entries.context_json", (conn) => {
|
|
69
|
+
if (tableExists(conn, "memory_entries") && !columnExists(conn, "memory_entries", "context_json")) {
|
|
70
|
+
conn.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { ensureOnce, ensureMemoryContextColumn, tableExists, columnExists };
|
|
@@ -139,6 +139,37 @@ function resolveVariantCandidates(options) {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/*
|
|
143
|
+
* usage/설명은 사람 출력 전용이다 — JSON 결과(schemaVersion v1)와 decision 어휘는
|
|
144
|
+
* 손대지 않는다. README·`agentlas help`가 광고하는 `agentlas variant resolve`는
|
|
145
|
+
* 인자 없이 부르면 항상 decision=error 로 끝나는데, 그동안 사람에게 보인 건
|
|
146
|
+
* 내부 enum 한 줄(`error: EXACT_BASE_RELEASE_REQUIRED`) 뿐이라 필요한 플래그
|
|
147
|
+
* 이름이 소스 주석에만 존재했다. 그래서 (1) error 코드마다 사람이 읽고 바로
|
|
148
|
+
* 고칠 수 있는 사유 + usage 를 붙이고, (2) help 서브커맨드를 뚫는다.
|
|
149
|
+
*/
|
|
150
|
+
const VARIANT_USAGE_LINES = Object.freeze([
|
|
151
|
+
"usage: agentlas variant resolve [candidates.json] [--base-release <agent-release-id>]",
|
|
152
|
+
" [--prefer <variant-id>] [--no-base-only] [--json]",
|
|
153
|
+
" --base-release <id> required: the exact base agent release to resolve against.",
|
|
154
|
+
" May instead come from candidates.json's baseAgentReleaseId.",
|
|
155
|
+
" candidates.json variant candidates: a JSON array, or {baseAgentReleaseId, candidates[]}.",
|
|
156
|
+
" --prefer <id> preferred variant; picking another one reports decision=fallback.",
|
|
157
|
+
" --no-base-only fail instead of falling back to the base release with no Experience Pack.",
|
|
158
|
+
" --json emit the agentlas.terminal-variant-resolution.v1 document.",
|
|
159
|
+
"Local advisory preview only: it never authorizes execution, rental, or reputation.",
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const VARIANT_ERROR_HINTS = Object.freeze({
|
|
163
|
+
EXACT_BASE_RELEASE_REQUIRED:
|
|
164
|
+
"no base agent release was given — pass --base-release <agent-release-id>, or a candidates.json carrying baseAgentReleaseId.",
|
|
165
|
+
NO_ELIGIBLE_VARIANT_AND_NO_BASE_FALLBACK:
|
|
166
|
+
"no candidate survived the checks above and --no-base-only forbids the base-only fallback.",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function variantUsage() {
|
|
170
|
+
return VARIANT_USAGE_LINES.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
142
173
|
function renderVariantResolution(result) {
|
|
143
174
|
const lines = [
|
|
144
175
|
`VARIANT RESOLUTION: ${result.decision}`,
|
|
@@ -148,17 +179,29 @@ function renderVariantResolution(result) {
|
|
|
148
179
|
if (result.decision === "selected") lines.push(`selected: ${result.selectedVariantId}`);
|
|
149
180
|
else if (result.decision === "fallback") lines.push(`fallback selected: ${result.selectedVariantId}`);
|
|
150
181
|
else if (result.decision === "base-only") lines.push(`base-only: ${result.baseAgentReleaseId} (no Experience Pack attached)`);
|
|
151
|
-
else
|
|
182
|
+
else {
|
|
183
|
+
lines.push(`error: ${result.code}`);
|
|
184
|
+
const hint = VARIANT_ERROR_HINTS[result.code];
|
|
185
|
+
if (hint) lines.push(hint);
|
|
186
|
+
}
|
|
152
187
|
if (result.fallbackOrder.length) lines.push(`next fallbacks: ${result.fallbackOrder.join(", ")}`);
|
|
153
188
|
for (const excluded of result.excluded) lines.push(`excluded only ${excluded.variantId}: ${excluded.reasons.join(", ")}`);
|
|
154
189
|
lines.push("Required MCP shortages exclude only the affected variant; they never create an agent-wide shortage.");
|
|
190
|
+
// 실패했을 때만 usage를 붙인다 — 성공 출력은 v1과 바이트 동일하게 유지한다.
|
|
191
|
+
if (result.decision === "error") lines.push("", variantUsage());
|
|
155
192
|
return lines.join("\n");
|
|
156
193
|
}
|
|
157
194
|
|
|
158
195
|
function cmdVariant(options) {
|
|
159
196
|
const args = options.args || [];
|
|
160
197
|
const sub = args[0] || "resolve";
|
|
161
|
-
|
|
198
|
+
// help 탈출구: 상위 라우터가 -h/--help 를 "help" 로 정규화하지만, 직접 호출도
|
|
199
|
+
// 받도록 셋 다 인정한다. usage는 성공(exit 0)이지 알 수 없는 서브커맨드가 아니다.
|
|
200
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
201
|
+
(options.out || console.log)(variantUsage());
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
if (sub !== "resolve") throw new Error(`unknown variant subcommand: ${sub} (resolve|help)`);
|
|
162
205
|
const flags = parseSimpleFlags(args.slice(1));
|
|
163
206
|
let candidates = [];
|
|
164
207
|
let baseAgentReleaseId = flags["base-release"] || null;
|
|
@@ -192,5 +235,6 @@ module.exports = {
|
|
|
192
235
|
validateVariantCandidate,
|
|
193
236
|
resolveVariantCandidates,
|
|
194
237
|
renderVariantResolution,
|
|
238
|
+
variantUsage,
|
|
195
239
|
cmdVariant,
|
|
196
240
|
};
|
|
@@ -40,6 +40,13 @@ const USAGE = Object.freeze({
|
|
|
40
40
|
call: 'usage: agentlas call "<agent-slugs>" "<context>"',
|
|
41
41
|
connect: "usage: agentlas connect [status|telegram|help]",
|
|
42
42
|
hep: "usage: agentlas hep <subcommand> [args]",
|
|
43
|
+
// 소스 스코프를 지키는 스태핑 표면 3종. 이름이 곧 계약이라 usage 도 스코프를
|
|
44
|
+
// 문장으로 적는다 — 이전에는 별칭이 스코프를 버려 hep-cloud 가 자산 보관함
|
|
45
|
+
// usage 를, hep-hub 가 검색 목록을 뱉었다(2026-07-28 수리).
|
|
46
|
+
"hep-network": 'usage: agentlas hep-network "<request>" # Local + owner Cloud + public Hub, federated by Core',
|
|
47
|
+
"hep-local": 'usage: agentlas hep-local "<request>" # registered Local agents only',
|
|
48
|
+
"hep-cloud": 'usage: agentlas hep-cloud "<request>" # owner Agent Cloud agents only',
|
|
49
|
+
"hep-hub": 'usage: agentlas hep-hub "<request>" # public Agentlas Hub agents only',
|
|
43
50
|
hephaestus: "usage: agentlas hephaestus <subcommand> [args]",
|
|
44
51
|
journal: "usage: agentlas journal <status|verify|repair|gate> --run-id <id> | --journal <path>",
|
|
45
52
|
"legacy-network": 'usage: agentlas legacy-network "<request>"',
|
|
@@ -31,13 +31,9 @@ function loadArch() {
|
|
|
31
31
|
return _arch;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
|
|
38
|
-
}
|
|
39
|
-
} catch { /* ignore */ }
|
|
40
|
-
}
|
|
34
|
+
// 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다 — 이 보정이 매 턴 공유 DB 에
|
|
35
|
+
// 쓰기 락을 잡고 있었다(2026-07-28). 소유자는 데스크탑 스키마다.
|
|
36
|
+
const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
|
|
41
37
|
|
|
42
38
|
const SECRET_RE = [/\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}/, /AKIA[0-9A-Z]{16}/, /ghp_[A-Za-z0-9]{20,}/, /xox[baprs]-[A-Za-z0-9-]{10,}/, /-----BEGIN [A-Z ]*PRIVATE KEY-----/, /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|bearer)\b\s*[:=]\s*\S+/i];
|
|
43
39
|
|
|
@@ -32,13 +32,9 @@ function sha(value) {
|
|
|
32
32
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
db.exec("ALTER TABLE memory_entries ADD COLUMN context_json TEXT NOT NULL DEFAULT '{}'");
|
|
39
|
-
}
|
|
40
|
-
} catch { /* ignore */ }
|
|
41
|
-
}
|
|
35
|
+
// 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다 — 이 보정이 매 턴 공유 DB 에
|
|
36
|
+
// 쓰기 락을 잡고 있었다(2026-07-28). 소유자는 데스크탑 스키마다.
|
|
37
|
+
const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
|
|
42
38
|
|
|
43
39
|
function prefsLangCli() {
|
|
44
40
|
try {
|
package/engine/project/state.cjs
CHANGED
|
@@ -341,15 +341,16 @@ function activeProjectPath(db, options = {}) {
|
|
|
341
341
|
return result.activated ? root : null;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
// v1 시그니처 보존: permission/reason은 받되 절대 초기화 권한으로 쓰지 않는다.
|
|
345
344
|
function ensureTerminalProjectForExecutionCli(db, projectPath, permission = "write", reason = "terminal-first-contact") {
|
|
346
345
|
const root = terminalProjectCandidateCli(projectPath);
|
|
347
346
|
if (!root) return null;
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
347
|
+
// A real write-capable execution is the product's first-contact boundary.
|
|
348
|
+
// Install the same private, merge-only Core infrastructure in whichever
|
|
349
|
+
// project the user opened; never key this behavior to the Agentlas_F repo.
|
|
350
|
+
if (permission === "write" || permission === "full") {
|
|
351
|
+
return initializeTerminalProjectCli(db, root, reason);
|
|
352
|
+
}
|
|
353
|
+
// Read-only inspection remains passive and never creates files.
|
|
353
354
|
const active = activeProjectPath(db, { projectPath: root });
|
|
354
355
|
if (!active) return null;
|
|
355
356
|
return initializedAgentlasProjectPathCli(root);
|
|
@@ -117,6 +117,27 @@ class Orchestrator extends EventEmitter {
|
|
|
117
117
|
if (!session) return;
|
|
118
118
|
if (session.isBusy()) session.kill();
|
|
119
119
|
this.sessions.delete(key);
|
|
120
|
+
|
|
121
|
+
// 부모를 지워도 자식 세션은 살아 있다(실행 중이며 병렬 슬롯을 쥔 채일 수 있다).
|
|
122
|
+
// 자식의 parent 포인터가 지워진 세션을 계속 가리키면 list()의 루트 판정
|
|
123
|
+
// (!s.parent)에서 탈락하고, 어떤 루트에서도 닿지 않아 /sessions·/tree 에서
|
|
124
|
+
// 통째로 사라진다 — 실행 중인데 보이지도 끌 수도 없는 유령 세션이 된다.
|
|
125
|
+
// 그래서 세션이 맵을 떠날 때 트리를 함께 정리한다: 남은 자식은 살아 있는
|
|
126
|
+
// 조부모로 승계하고(없으면 루트로 승격), 지워진 키는 부모 목록에서 뗀다.
|
|
127
|
+
const grandparent = session.parent && this.sessions.has(session.parent.key) ? session.parent : null;
|
|
128
|
+
for (const childKey of session.children) {
|
|
129
|
+
const child = this.sessions.get(childKey);
|
|
130
|
+
if (!child) continue;
|
|
131
|
+
child.parent = grandparent;
|
|
132
|
+
if (grandparent && !grandparent.children.includes(childKey)) grandparent.children.push(childKey);
|
|
133
|
+
}
|
|
134
|
+
session.children = [];
|
|
135
|
+
if (session.parent) {
|
|
136
|
+
const siblings = session.parent.children;
|
|
137
|
+
const at = siblings.indexOf(key);
|
|
138
|
+
if (at >= 0) siblings.splice(at, 1);
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
if (this.activeKey === key) {
|
|
121
142
|
const rest = [...this.sessions.keys()];
|
|
122
143
|
this.activeKey = rest.length ? rest[rest.length - 1] : null;
|
|
@@ -124,14 +145,31 @@ class Orchestrator extends EventEmitter {
|
|
|
124
145
|
this.emit("sessions-changed");
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
/**
|
|
149
|
+
* 전 세션 브로드캐스트 — {sent, skipped}를 반환한다(throw 하지 않는다).
|
|
150
|
+
*
|
|
151
|
+
* WHY: 예전엔 루프 안에서 sendTo()의 throw가 그대로 올라갔다. 동시 상한(기본 4)보다
|
|
152
|
+
* 유휴 세션이 많으면 앞의 몇 개는 이미 프롬프트를 받아 실행을 시작한 뒤 상한 세션에서
|
|
153
|
+
* 터졌고, 그때까지 모은 sent 배열은 스택과 함께 버려졌다. 호출자(REPL)는 상한 에러
|
|
154
|
+
* 한 줄만 찍어 "전부 실패"로 보고했지만 실제로는 4개 세션이 그 지시를 받아 토큰을
|
|
155
|
+
* 쓰고 쓰기 작업까지 할 수 있는 상태였다 — 부분 성공을 전면 실패로 오보하는 것은
|
|
156
|
+
* 브로드캐스트에서 가장 위험한 거짓말이다.
|
|
157
|
+
* 그래서 실패는 세션 단위로 모으고, 실제 전달된 목록은 무슨 일이 있어도 반환한다.
|
|
158
|
+
* (sendTo/spawn의 "상한 초과는 정직한 거부" 계약 자체는 그대로 둔다.)
|
|
159
|
+
*/
|
|
127
160
|
broadcast(prompt) {
|
|
128
161
|
const sent = [];
|
|
162
|
+
const skipped = [];
|
|
129
163
|
for (const [key, session] of this.sessions) {
|
|
130
164
|
if (session.status === "killed") continue;
|
|
131
|
-
|
|
132
|
-
|
|
165
|
+
try {
|
|
166
|
+
this.sendTo(key, prompt);
|
|
167
|
+
sent.push(key);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
skipped.push({ key, error: String((e && e.message) || e) });
|
|
170
|
+
}
|
|
133
171
|
}
|
|
134
|
-
return sent;
|
|
172
|
+
return { sent, skipped };
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
/** 세션 표: [{key, active, agent, status, elapsed, lastLine, parentKey, depth}] */
|
|
@@ -88,13 +88,10 @@ function contextLine(json) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
} catch { /* 앱 마이그레이션 중이면 다음 턴에 */ }
|
|
97
|
-
}
|
|
91
|
+
// 사본 3벌을 하나로 합치고 커넥션당 1회로 줄였다. 이 파일의 호출부는 **매 턴** 도는
|
|
92
|
+
// 자리라(cliMemoryContext), 여기가 공유 DB 에 쓰기 락을 가장 자주 잡던 지점이었다.
|
|
93
|
+
// 실패는 여전히 다음 호출에서 재시도된다 — 실패를 "완료"로 기억하지 않는다.
|
|
94
|
+
const { ensureMemoryContextColumn } = require("../core/schema-ensure.cjs");
|
|
98
95
|
|
|
99
96
|
/** Context Map 슬라이스 — Core 부재 시 정직하게 빈 문자열 (조작된 지도 금지). */
|
|
100
97
|
function cliProjectContextSlice(projectPath, task) {
|
|
@@ -124,6 +124,25 @@ class Session extends EventEmitter {
|
|
|
124
124
|
return result;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 이미 동의된 시스템 MCP 서버 목록. `full` 권한이 아니면 빈 배열이다.
|
|
129
|
+
*
|
|
130
|
+
* 권한 확인을 여기서 한 번 더 하는 이유: native-host 도 `full` 게이트를 갖고
|
|
131
|
+
* 있지만, 권한이 낮은 턴에서 목록을 **읽는 것 자체를** 하지 않는 편이 낫다.
|
|
132
|
+
* 읽지 않은 값은 실수로 새 나갈 수 없다.
|
|
133
|
+
*/
|
|
134
|
+
_consentedMcpServers() {
|
|
135
|
+
if (permissions.normalize(this.permission) !== "full") return [];
|
|
136
|
+
try {
|
|
137
|
+
const consent = require("../mcp/consent.cjs");
|
|
138
|
+
return consent.readConsentedSystemMcpServers(this.db, { env: process.env }) || [];
|
|
139
|
+
} catch {
|
|
140
|
+
// 동의 상태를 못 읽으면 MCP 없이 간다. 여기서 실패해 턴을 죽이면,
|
|
141
|
+
// MCP 는 부가 기능인데 대화 자체가 불가능해진다.
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
127
146
|
async _runTurn(prompt) {
|
|
128
147
|
this.status = "running";
|
|
129
148
|
this.startedAt = Date.now();
|
|
@@ -196,6 +215,19 @@ class Session extends EventEmitter {
|
|
|
196
215
|
session: { ...this.runtimeSession },
|
|
197
216
|
model: this.runtime.model,
|
|
198
217
|
effort: this.runtime.effort,
|
|
218
|
+
// 사용자가 이미 동의한 MCP 서버를 턴에 싣는다.
|
|
219
|
+
//
|
|
220
|
+
// 여기가 비어 있어서 챗 경로의 MCP 가 통째로 죽어 있었다(2026-07-28 확인:
|
|
221
|
+
// `mcpServers` 를 넘기는 호출자가 0곳). native-host 는 `full` 권한에서만
|
|
222
|
+
// 주입하도록 이미 게이팅돼 있었는데, 그 분기에 도달할 데이터를 아무도
|
|
223
|
+
// 채우지 않았다 — `agentlas mcp probe` 가 connected 를 찍어도 실제 턴에서는
|
|
224
|
+
// 그 서버를 쓸 수 없었다.
|
|
225
|
+
//
|
|
226
|
+
// 여기서 새로 묻지 않는다. `readConsentedSystemMcpServers` 는 이미 받아 둔
|
|
227
|
+
// 동의 영수증과 지문이 **지금도 일치하는** 서버만 돌려준다. 동의가 없으면
|
|
228
|
+
// 빈 배열이고, native-host 가 명시적 빈 격리로 간다. 턴 도중에 동의를 묻는
|
|
229
|
+
// 것은 사용자가 답할 수 없는 자리에서 묻는 것이라 하지 않는다.
|
|
230
|
+
mcpServers: this._consentedMcpServers(),
|
|
199
231
|
onSpawn: (child) => { this._child = child; },
|
|
200
232
|
};
|
|
201
233
|
if (this._spawnImpl) req.spawn = this._spawnImpl;
|
package/engine/storm/swarm.cjs
CHANGED
|
@@ -427,6 +427,18 @@ function create(deps) {
|
|
|
427
427
|
if (args[i] === "--parallel" || args[i] === "-n") concurrency = Number(args[++i]);
|
|
428
428
|
else rest.push(args[i]);
|
|
429
429
|
}
|
|
430
|
+
// storm.cjs stormRun 의 leading-dash 가드와 동일 계약. 여기서 안 막으면 미지원·
|
|
431
|
+
// 오타 플래그(--permission, --model …)가 rest 에 남아 rest.join(" ") 로 목표가
|
|
432
|
+
// 되어버린다: 유료 플래너가 플래그 텍스트를 목표로 실제 실행되고, 플래그가
|
|
433
|
+
// 의도한 동작(예: 권한 상승)은 조용히 일어나지 않는다. 목표는 프롬프트까지
|
|
434
|
+
// 오염된다. 그래서 소비되지 않은 대시 토큰은 실행 전에 fail-closed 로 거절한다.
|
|
435
|
+
const strayFlag = rest.find((token) => String(token).startsWith("-"));
|
|
436
|
+
if (strayFlag) {
|
|
437
|
+
const ui = executionContext.ui || newUi();
|
|
438
|
+
ui.error(`unknown option ${strayFlag} — swarm accepts: --parallel N | -n N, --runtime <kind>`);
|
|
439
|
+
process.exitCode = 1;
|
|
440
|
+
return { ok: false, error: "unknown-option" };
|
|
441
|
+
}
|
|
430
442
|
const r = await swarmRun(db, rest.join(" "), { ...executionContext, concurrency, runtimeOverride });
|
|
431
443
|
if (!r.ok) process.exitCode = 1;
|
|
432
444
|
return r;
|
package/engine/ui/palette.cjs
CHANGED
|
@@ -44,6 +44,13 @@ const SLASH_COMMANDS = [
|
|
|
44
44
|
{ command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
|
|
45
45
|
{ command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
|
|
46
46
|
{ command: "/taskforce", args: "<request>", ko: "임시 태스크포스 편성", en: "Assemble a task force" },
|
|
47
|
+
// 소스 스코프가 이름에 붙은 스태핑 3종. 스코프를 문장에 적는다 — 예전에는
|
|
48
|
+
// 별칭이 스코프를 버려 /hep-cloud 가 자산 보관함으로, /hep-hub 가 검색으로
|
|
49
|
+
// 갔다(2026-07-28 수리). 팔레트 문구가 곧 사용자에게 하는 약속이다.
|
|
50
|
+
{ command: "/hep-network", args: "\"<request>\"", ko: "로컬+오너 클라우드+공개 Hub 연합 편성", en: "Staff across Local + owner Cloud + public Hub" },
|
|
51
|
+
{ command: "/hep-local", args: "\"<request>\"", ko: "등록된 로컬 에이전트만으로 편성", en: "Staff from registered Local agents only" },
|
|
52
|
+
{ command: "/hep-cloud", args: "\"<request>\"", ko: "오너 Agent Cloud만으로 편성", en: "Staff from owner Agent Cloud only" },
|
|
53
|
+
{ command: "/hep-hub", args: "\"<request>\"", ko: "공개 Hub 에이전트만으로 편성", en: "Staff from public Hub agents only" },
|
|
47
54
|
{ command: "/build", args: "\"<request>\"", ko: "에이전트·팀 제작/수리/패키징", en: "Build, repair or package an agent or team" },
|
|
48
55
|
{ command: "/call", args: "\"a,b\" \"<ctx>\"", ko: "지정 에이전트 호출", en: "Call named agents" },
|
|
49
56
|
{ command: "/route", args: "\"<req>\"", ko: "최적 에이전트 라우팅", en: "Route to the best agent" },
|
|
@@ -61,7 +68,11 @@ const SLASH_COMMANDS = [
|
|
|
61
68
|
{ command: "/variant", args: "resolve", ko: "로컬 변형 선택", en: "Local variant selection" },
|
|
62
69
|
{ command: "/memory", args: "<sub>", ko: "메모리", en: "Memory" },
|
|
63
70
|
{ command: "/evolve", args: "", ko: "프롬프트 진화 제안", en: "Prompt-evolution proposals" },
|
|
64
|
-
|
|
71
|
+
// 데스크탑의 `ontology` 는 Core 의 지식·메모리 **런타임**(임베딩 포함)이고, 터미널의
|
|
72
|
+
// 이것은 **이 프로젝트의 지식 소스 등록부**다. 서로 다른 것이 같은 이름을 쓰고 있어
|
|
73
|
+
// (감사 D6) 라벨이라도 정확해야 한다 — 명령 이름은 사용자 습관과 스크립트가 걸려
|
|
74
|
+
// 있어 바꾸지 않는다. Core 의 지식 런타임은 터미널에 아직 미노출이다(결함 아님).
|
|
75
|
+
{ command: "/ontology", args: "", ko: "프로젝트 지식 소스 등록", en: "Project knowledge sources" },
|
|
65
76
|
{ command: "/career-graph", args: "", ko: "소스 라우팅 그래프", en: "Source routing graph" },
|
|
66
77
|
{ command: "/journal", args: "<sub>", ko: "Stormbreaker 실행 일지", en: "Stormbreaker run journal" },
|
|
67
78
|
{ command: "/project", args: "[status|init]", ko: ".agentlas 프로젝트 상태", en: "Private project state" },
|
|
@@ -77,7 +88,8 @@ const SLASH_COMMANDS = [
|
|
|
77
88
|
{ command: "/update", args: "", ko: "npm 업데이트 확인", en: "npm update check" },
|
|
78
89
|
{ command: "/version", args: "", ko: "버전", en: "Version" },
|
|
79
90
|
{ command: "/logout", args: "", ko: "로그아웃", en: "Sign out" },
|
|
80
|
-
|
|
91
|
+
// 대화가 있으면 --yes 없이는 거절한다(챗/메시지 CASCADE 삭제) — 팔레트에도 노출.
|
|
92
|
+
{ command: "/uninstall", args: "<slug> [--yes]", ko: "에이전트 제거", en: "Uninstall an agent" },
|
|
81
93
|
{ command: "/quit", args: "", ko: "종료", en: "Quit" },
|
|
82
94
|
{ command: "/exit", args: "", ko: "종료", en: "Quit" },
|
|
83
95
|
];
|
package/engine/ui/renderer.cjs
CHANGED
|
@@ -77,6 +77,43 @@ class Renderer {
|
|
|
77
77
|
case "task-result": ui.applyTaskResult(ev.name, ev.result, ev.id); return;
|
|
78
78
|
case "cost": ui.cost(ev.usage); return;
|
|
79
79
|
case "queued": ui.line(ui.c.dim(` ↳ queued for next turn: ${ev.text}`)); return;
|
|
80
|
+
|
|
81
|
+
/* ── 펜스 영수증 (apply-fences.cjs) ──────────────────────────────────
|
|
82
|
+
* WHY: 펜스 블록은 cleanText 에서 통째로 제거되므로, 이 case 들이 없으면
|
|
83
|
+
* default 로 떨어져 화면에 아무 흔적도 남지 않는다. 그 결과 이미 enabled
|
|
84
|
+
* 상태로 등록된 반복 자동화(데몬/데스크탑 스케줄러가 실제로 실행한다)나
|
|
85
|
+
* 스폰된 위임 세션이 사용자 모르게 생기고, 에이전트의 확인 질문(ask)은
|
|
86
|
+
* 본문에서 삭제된 채 영영 묻지 않는다. 부작용에는 반드시 영수증이 따른다 —
|
|
87
|
+
* 새 펜스 이벤트를 apply-fences 에 추가하면 여기에도 case 를 추가할 것. */
|
|
88
|
+
case "ask": {
|
|
89
|
+
const p = ev.payload || {};
|
|
90
|
+
ui.ensureNl();
|
|
91
|
+
if (p.header) ui.line(ui.c.dim(` ${p.header}`));
|
|
92
|
+
ui.warn(p.question || "(question)");
|
|
93
|
+
(p.options || []).forEach((o, i) => {
|
|
94
|
+
const desc = o && o.description ? ` — ${o.description}` : "";
|
|
95
|
+
ui.line(ui.c.dim(` ${i + 1}. ${(o && o.label) || ""}${desc}`));
|
|
96
|
+
});
|
|
97
|
+
ui.line(ui.c.dim(` ↳ 답을 그대로 입력하세요${p.multiSelect ? " (복수 선택 가능)" : ""}`));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
case "automation-registered": {
|
|
101
|
+
const steps = ev.stepsIgnored ? ` · steps ${ev.stepsIgnored}개 무시(터미널은 그래프 합성 없음)` : "";
|
|
102
|
+
ui.ok(`automation registered: ${ev.name} · ${ev.schedule} · next ${ev.nextRunAt}${steps}`);
|
|
103
|
+
// 취소 경로를 함께 제시한다. 실제 서브커맨드는 off (commands/automation.cjs:125).
|
|
104
|
+
ui.line(ui.c.dim(` ↳ agentlas automation list · agentlas automation off ${String(ev.id || "").slice(0, 8)}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case "automation-refused": ui.warn(`automation refused: ${ev.name} — ${ev.reason}`); return;
|
|
108
|
+
case "delegate-spawned":
|
|
109
|
+
ui.ok(`delegate spawned: ${ev.target} → ${ev.key}`);
|
|
110
|
+
return;
|
|
111
|
+
case "delegate-refused": ui.warn(`delegate refused: ${ev.target} — ${ev.reason}`); return;
|
|
112
|
+
case "fence-error": ui.error(`fence: ${ev.text}`); return;
|
|
113
|
+
case "memory-curated":
|
|
114
|
+
ui.line(ui.c.dim(` ↳ memory: ${ev.written}/${ev.candidates} written (permission: ${ev.permission})`));
|
|
115
|
+
return;
|
|
116
|
+
|
|
80
117
|
default: return;
|
|
81
118
|
}
|
|
82
119
|
}
|
package/engine/ui/repl.cjs
CHANGED
|
@@ -616,8 +616,14 @@ function handleSlash(ctx, cmdline, api) {
|
|
|
616
616
|
|
|
617
617
|
case "broadcast": {
|
|
618
618
|
if (!restStr) throw new Error("Usage: /broadcast <message>");
|
|
619
|
-
|
|
619
|
+
/*
|
|
620
|
+
* broadcast 는 {sent, skipped} 를 준다. 전달된 목록을 먼저 찍고 못 보낸 세션은
|
|
621
|
+
* 사유와 함께 경고로 덧붙인다 — 예전엔 상한 throw 가 이 catch 로 떨어져 에러 한
|
|
622
|
+
* 줄만 보였고, 이미 지시를 받아 돌기 시작한 세션이 화면에 전혀 안 나왔다.
|
|
623
|
+
*/
|
|
624
|
+
const { sent, skipped } = orch.broadcast(restStr);
|
|
620
625
|
ctx.out(ui.c.dim(`→ ${sent.join(", ") || "(none)"}`));
|
|
626
|
+
for (const s of skipped) ui.warn(`${s.key} ${en ? "not sent" : "미전송"}: ${s.error}`);
|
|
621
627
|
return;
|
|
622
628
|
}
|
|
623
629
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* workforce/concurrency — 워크포스 워커 동시 실행 수(스웜 크기)의 사양 기반 추천값.
|
|
4
|
+
*
|
|
5
|
+
* 워커 1명 = captureRuntime을 통해 스폰되는 실제 CLI 자식 프로세스(engine/workforce/
|
|
6
|
+
* capture.cjs)다. 이전에는 사용자가 --parallel/-n을 안 주면 컴퓨터 사양과 무관하게
|
|
7
|
+
* 고정 3(상한 8)을 썼다 — 코어 2개짜리 저사양 기기에서 3개 CLI 자식이 한꺼번에 뜨면
|
|
8
|
+
* 과다 산정, 32코어 워크스테이션에서도 항상 3으로 저평가되는 양방향 문제였다.
|
|
9
|
+
*
|
|
10
|
+
* 데스크탑(electron/store/concurrency.ts)과 동일한 원리(코어 2개는 OS/자식 자신에게
|
|
11
|
+
* 남기고, 에이전트당 RAM ~2GB + 여유 4GB로 추정)를 쓰되, 상한(HARD_MAX)은 터미널
|
|
12
|
+
* 자체의 기존 안전선(8)을 그대로 유지한다 — 이건 provider가 바뀌어도 변하지 않는
|
|
13
|
+
* Agentlas 자체 정책 상수이지, 하드코딩 금지 대상인 "외부에서 바뀌는 값"이 아니다.
|
|
14
|
+
*/
|
|
15
|
+
const os = require("node:os");
|
|
16
|
+
|
|
17
|
+
const HARD_MAX = 8;
|
|
18
|
+
|
|
19
|
+
function getSystemSpecs() {
|
|
20
|
+
let cores = 4;
|
|
21
|
+
let totalMemGB = 8;
|
|
22
|
+
try {
|
|
23
|
+
cores = Math.max(1, os.cpus().length);
|
|
24
|
+
} catch {
|
|
25
|
+
// fall back
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
totalMemGB = os.totalmem() / 1024 ** 3;
|
|
29
|
+
} catch {
|
|
30
|
+
// fall back
|
|
31
|
+
}
|
|
32
|
+
return { cores, totalMemGB };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function recommendedConcurrency(specs = getSystemSpecs()) {
|
|
36
|
+
const coreBound = Math.max(1, specs.cores - 2);
|
|
37
|
+
const memBound = Math.max(1, Math.floor((specs.totalMemGB - 4) / 2));
|
|
38
|
+
return Math.max(1, Math.min(coreBound, memBound, HARD_MAX));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { HARD_MAX, getSystemSpecs, recommendedConcurrency };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|