agentlas 1.0.11 → 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 +67 -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 +62 -8
- 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 +989 -114
- package/engine/agentlas-workload-routing.cjs +61 -7
- package/engine/agentlas.cjs +8 -0
- package/engine/automation/daemon.cjs +76 -30
- package/engine/automation/schedule.cjs +16 -0
- package/engine/automation/store.cjs +92 -12
- package/engine/bootstrap-schema.sql +788 -29
- package/engine/commands/automation.cjs +8 -0
- package/engine/commands/chats.cjs +6 -1
- package/engine/commands/doctor.cjs +23 -1
- package/engine/commands/firm.cjs +66 -4
- package/engine/commands/help.cjs +31 -7
- 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/list.cjs +25 -1
- package/engine/commands/open.cjs +5 -1
- package/engine/commands/run.cjs +77 -13
- 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/firms/orchestrate.cjs +10 -3
- 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/runtimes/overrides.cjs +91 -22
- package/engine/runtimes/resolve.cjs +38 -8
- package/engine/runtimes/roles.cjs +162 -0
- package/engine/sessions/orchestrator.cjs +43 -4
- package/engine/sessions/prompt.cjs +4 -7
- package/engine/sessions/session.cjs +88 -20
- package/engine/storm/swarm.cjs +40 -12
- package/engine/ui/palette.cjs +52 -0
- package/engine/ui/renderer.cjs +37 -0
- package/engine/ui/repl.cjs +78 -10
- package/engine/workforce/capture.cjs +199 -14
- package/engine/workforce/concurrency.cjs +41 -0
- package/engine/workforce/deps.cjs +145 -29
- package/package.json +1 -1
package/engine/commands/run.cjs
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* run — 원샷 실행: agentlas run [agent] [prompt…]
|
|
4
4
|
* -p | --print 최종 답만 stdout에 (스트리밍 UI 없음)
|
|
5
5
|
* --runtime <kind> claude-code | codex | gemini
|
|
6
|
+
* --model <id> exact provider model id
|
|
7
|
+
* --effort <level> none | minimal | low | medium | high | xhigh | max
|
|
8
|
+
* --tier <tier> economy | balanced | frontier (requires --model)
|
|
6
9
|
* --permission <level> read | write | full
|
|
7
10
|
* 프롬프트가 없고 stdin이 TTY가 아니면 stdin을 읽는다.
|
|
8
11
|
*
|
|
@@ -10,15 +13,21 @@
|
|
|
10
13
|
* 판정(resolveAutoRoute)이 최적 에이전트를 고른다 — 어휘 점수는 후보 모집 전용이다.
|
|
11
14
|
* 판정 런타임이 없으면 기본 에이전트로 정직 폴백하되 note를 stderr에 반드시 출력한다
|
|
12
15
|
* (조용한 오라우팅/폴백 금지 — 오너 결정).
|
|
13
|
-
* 런타임 사다리: 명시
|
|
14
|
-
*
|
|
16
|
+
* 런타임 사다리: 명시 핀 > 에이전트별 오버라이드(agent_runtime_overrides) >
|
|
17
|
+
* model_roles[orchestrator] > active_runtime > detected (runtimes/overrides.cjs).
|
|
15
18
|
*/
|
|
16
19
|
const { findAgent, listAgents } = require("../agents/registry.cjs");
|
|
17
|
-
const {
|
|
18
|
-
|
|
20
|
+
const {
|
|
21
|
+
resolveRuntimeForAgent,
|
|
22
|
+
unavailableOverrideNote,
|
|
23
|
+
unavailableRoleNote,
|
|
24
|
+
} = require("../runtimes/overrides.cjs");
|
|
19
25
|
const { Orchestrator } = require("../sessions/orchestrator.cjs");
|
|
20
26
|
const { Renderer } = require("../ui/renderer.cjs");
|
|
21
27
|
const permissions = require("../agentlas-permissions.cjs");
|
|
28
|
+
const { EFFORTS, TIERS } = require("../agentlas-workload-routing.cjs");
|
|
29
|
+
const { projectCwd } = require("../project/paths.cjs");
|
|
30
|
+
const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
|
|
22
31
|
|
|
23
32
|
function readStdin() {
|
|
24
33
|
return new Promise((resolve) => {
|
|
@@ -30,11 +39,22 @@ function readStdin() {
|
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
function parseArgs(args) {
|
|
33
|
-
const out = {
|
|
42
|
+
const out = {
|
|
43
|
+
print: false,
|
|
44
|
+
runtime: null,
|
|
45
|
+
model: null,
|
|
46
|
+
effort: null,
|
|
47
|
+
tier: null,
|
|
48
|
+
permission: null,
|
|
49
|
+
rest: [],
|
|
50
|
+
};
|
|
34
51
|
for (let i = 0; i < args.length; i++) {
|
|
35
52
|
const a = args[i];
|
|
36
53
|
if (a === "-p" || a === "--print") out.print = true;
|
|
37
54
|
else if (a === "--runtime") out.runtime = args[++i];
|
|
55
|
+
else if (a === "--model") out.model = args[++i];
|
|
56
|
+
else if (a === "--effort") out.effort = args[++i];
|
|
57
|
+
else if (a === "--tier") out.tier = args[++i];
|
|
38
58
|
else if (a === "--permission") out.permission = args[++i];
|
|
39
59
|
else out.rest.push(a);
|
|
40
60
|
}
|
|
@@ -49,6 +69,18 @@ async function runOnce(ctx, args) {
|
|
|
49
69
|
ctx.err(`unknown --permission ${parsed.permission} (use: read | write | full)`);
|
|
50
70
|
return 1;
|
|
51
71
|
}
|
|
72
|
+
if (parsed.effort && !EFFORTS.includes(String(parsed.effort))) {
|
|
73
|
+
ctx.err(`unknown --effort ${parsed.effort} (use: ${EFFORTS.join(" | ")})`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (parsed.tier && !TIERS.includes(String(parsed.tier))) {
|
|
77
|
+
ctx.err(`unknown --tier ${parsed.tier} (use: ${TIERS.join(" | ")})`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (parsed.tier && !parsed.model) {
|
|
81
|
+
ctx.err("--tier requires --model: Terminal never guesses a provider model id from a cost tier");
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
52
84
|
const db = ctx.db();
|
|
53
85
|
|
|
54
86
|
let agent = null;
|
|
@@ -70,7 +102,15 @@ async function runOnce(ctx, args) {
|
|
|
70
102
|
// 기본 런타임을 먼저 확정한다 — 판정 러너(자동 라우팅)도 이 런타임을 쓴다.
|
|
71
103
|
let runtime;
|
|
72
104
|
try {
|
|
73
|
-
runtime =
|
|
105
|
+
runtime = resolveRuntimeForAgent({
|
|
106
|
+
db,
|
|
107
|
+
prefs: ctx.prefs,
|
|
108
|
+
explicit: parsed.runtime,
|
|
109
|
+
model: parsed.model,
|
|
110
|
+
effort: parsed.effort,
|
|
111
|
+
role: "orchestrator",
|
|
112
|
+
});
|
|
113
|
+
if (parsed.tier) runtime.modelTier = parsed.tier;
|
|
74
114
|
} catch (e) {
|
|
75
115
|
ctx.err(String((e && e.message) || e));
|
|
76
116
|
return 1;
|
|
@@ -103,21 +143,43 @@ async function runOnce(ctx, args) {
|
|
|
103
143
|
if (choice && choice.note) ctx.err(ctx.uiInstance.c.dim(choice.note));
|
|
104
144
|
}
|
|
105
145
|
|
|
106
|
-
// 에이전트가 확정된 뒤 에이전트별
|
|
107
|
-
|
|
146
|
+
// 에이전트가 확정된 뒤 에이전트별 오버라이드를 포함한 전체 사다리를 다시 해석한다.
|
|
147
|
+
// 명시 runtime/model/effort 핀은 항상 최상단이라 에이전트 기본값보다 우선한다.
|
|
148
|
+
if (agent && agent.id) {
|
|
108
149
|
try {
|
|
109
|
-
const layered = resolveRuntimeForAgent({
|
|
150
|
+
const layered = resolveRuntimeForAgent({
|
|
151
|
+
db,
|
|
152
|
+
prefs: ctx.prefs,
|
|
153
|
+
explicit: parsed.runtime,
|
|
154
|
+
model: parsed.model,
|
|
155
|
+
effort: parsed.effort,
|
|
156
|
+
role: "orchestrator",
|
|
157
|
+
agentId: agent.id,
|
|
158
|
+
});
|
|
110
159
|
if (layered.unavailableOverride) ctx.err(ctx.uiInstance.c.dim(unavailableOverrideNote(layered, ctx.lang)));
|
|
160
|
+
if (layered.unavailableRoleSelection) ctx.err(ctx.uiInstance.c.dim(unavailableRoleNote(layered, ctx.lang)));
|
|
161
|
+
if (parsed.tier) layered.modelTier = parsed.tier;
|
|
111
162
|
runtime = layered;
|
|
112
|
-
} catch {
|
|
163
|
+
} catch (e) {
|
|
164
|
+
ctx.err(String((e && e.message) || e));
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
113
167
|
}
|
|
114
168
|
|
|
169
|
+
const permission = permissions.normalize(parsed.permission || (ctx.prefs && ctx.prefs.permission) || "write");
|
|
170
|
+
const cwd = projectCwd();
|
|
171
|
+
try {
|
|
172
|
+
ensureTerminalProjectForExecutionCli(db, cwd, permission, "terminal-run");
|
|
173
|
+
} catch (e) {
|
|
174
|
+
ctx.err(String((e && e.message) || e));
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
115
177
|
const orch = new Orchestrator({ db, lang: ctx.lang });
|
|
116
178
|
const session = orch.spawn({
|
|
117
179
|
agent,
|
|
118
180
|
runtime,
|
|
119
|
-
permission
|
|
120
|
-
cwd
|
|
181
|
+
permission,
|
|
182
|
+
cwd,
|
|
121
183
|
title: prompt.slice(0, 60),
|
|
122
184
|
});
|
|
123
185
|
|
|
@@ -125,7 +187,9 @@ async function runOnce(ctx, args) {
|
|
|
125
187
|
if (!parsed.print) {
|
|
126
188
|
renderer = new Renderer(ctx.uiInstance);
|
|
127
189
|
renderer.attach(session, { replay: false });
|
|
128
|
-
ctx.err(ctx.uiInstance.c.dim(
|
|
190
|
+
ctx.err(ctx.uiInstance.c.dim(
|
|
191
|
+
`${agent.slug} · ${runtime.kind}${runtime.model ? ` · ${runtime.model}` : ""}${runtime.effort ? ` · ${runtime.effort}` : ""}`,
|
|
192
|
+
));
|
|
129
193
|
}
|
|
130
194
|
|
|
131
195
|
const res = await session.send(prompt);
|
|
@@ -10,20 +10,20 @@ const { userDataDir } = require("../core/paths.cjs");
|
|
|
10
10
|
const { RUNTIME_BIN, whichSync } = require("../runtimes/detect.cjs");
|
|
11
11
|
|
|
12
12
|
async function runWizard(ctx, rl) {
|
|
13
|
-
|
|
13
|
+
// 저장을 마법사 "밖"에서 하면 이미 "완료" 문구가 찍힌 뒤다. 저장 함수를 넘겨
|
|
14
|
+
// 성공 문구보다 먼저 쓰게 하고, updatePrefs가 던지는 실패를 그대로 올려보낸다
|
|
15
|
+
// (예전엔 반환값을 버려서 잠금 경합·읽기전용 디렉터리에서도 exit 0으로 거짓말).
|
|
16
|
+
return runOnboard({
|
|
14
17
|
ui: ctx.uiInstance,
|
|
15
18
|
rl,
|
|
16
19
|
helpers: { RUNTIME_BIN, which: whichSync },
|
|
17
|
-
|
|
18
|
-
if (result && result.onboarded) {
|
|
19
|
-
updatePrefs(userDataDir(), {
|
|
20
|
+
persist: (choices) => updatePrefs(userDataDir(), {
|
|
20
21
|
onboarded: true,
|
|
21
|
-
...(
|
|
22
|
-
...(
|
|
23
|
-
...(
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
22
|
+
...(choices.lang ? { language: choices.lang } : {}),
|
|
23
|
+
...(choices.runtime ? { runtime: choices.runtime } : {}),
|
|
24
|
+
...(choices.permission ? { permission: choices.permission } : {}),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async function run(ctx) {
|
|
@@ -36,7 +36,8 @@ async function run(ctx) {
|
|
|
36
36
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
37
37
|
try {
|
|
38
38
|
const result = await runWizard(ctx, rl);
|
|
39
|
-
|
|
39
|
+
// 저장까지 성공해야 0 — 설정이 디스크에 없으면 사용자에겐 아무것도 안 한 것과 같다.
|
|
40
|
+
return result && result.onboarded && result.saved ? 0 : 1;
|
|
40
41
|
} finally {
|
|
41
42
|
rl.close();
|
|
42
43
|
}
|
|
@@ -10,16 +10,12 @@
|
|
|
10
10
|
* stormbreaker-core-harness-unavailable 로 멈춘다 — 하네스의 로컬 모조 실행 금지.
|
|
11
11
|
* 런타임 부재는 resolveRuntime의 code="no_runtime" throw가 그대로 전파돼 exit 1.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 경로를 만들었다. 그 프로젝트-상태 기계는 아직 v2로 포팅되지 않았다. storm/swarm 이
|
|
16
|
-
* projectPath 를 쓰는 곳은 buildChildEnv 의 프로젝트 dotenv 스코핑뿐이고 부재 시
|
|
17
|
-
* fail-closed 의존이 없다 — 그래서 여기서는 cwd 를 projectPath 로 넘긴다
|
|
18
|
-
* (commands/workforce.cjs 와 동일한 결정·동일한 근거). 프로젝트-상태 모듈이 v2에
|
|
19
|
-
* 착륙하면 이 자리를 그 결과로 교체한다.
|
|
13
|
+
* Write-capable first contact now runs the canonical Core project bootstrap
|
|
14
|
+
* for the exact current folder. Read-only execution remains passive.
|
|
20
15
|
*/
|
|
21
16
|
const { buildStormDeps } = require("../storm/deps.cjs");
|
|
22
17
|
const { projectCwd } = require("../workforce/capture.cjs");
|
|
18
|
+
const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
|
|
23
19
|
|
|
24
20
|
function assertPermissionFlag(value) {
|
|
25
21
|
if (value && !["read", "write", "full"].includes(String(value))) {
|
|
@@ -62,11 +58,11 @@ async function run(ctx, args) {
|
|
|
62
58
|
const db = ctx.db();
|
|
63
59
|
const cwd = projectCwd();
|
|
64
60
|
const permission = resolvePermission(ctx);
|
|
61
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, permission, "terminal-storm") || cwd;
|
|
65
62
|
const storm = require("../storm/storm.cjs").create(buildStormDeps(ctx));
|
|
66
63
|
const r = await storm.cmdStorm(db, rest, runtimeOverride, {
|
|
67
64
|
cwd,
|
|
68
|
-
|
|
69
|
-
projectPath: cwd,
|
|
65
|
+
projectPath,
|
|
70
66
|
permission,
|
|
71
67
|
});
|
|
72
68
|
return r && r.ok ? 0 : 1;
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* agentlas-workload-routing 이 검증만 하고, 거부 시 fail-closed 다.
|
|
12
12
|
* 런타임 부재는 resolveRuntime의 code="no_runtime" throw가 그대로 전파돼 exit 1.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* commands/storm.cjs 파일 머리 주석과 동일한 결정·동일한 근거로 cwd 를 넘긴다.
|
|
14
|
+
* Write-capable first contact bootstraps the exact current project through Core.
|
|
16
15
|
*/
|
|
17
16
|
const { buildStormDeps } = require("../storm/deps.cjs");
|
|
18
17
|
const { projectCwd } = require("../workforce/capture.cjs");
|
|
18
|
+
const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
|
|
19
19
|
|
|
20
20
|
function assertPermissionFlag(value) {
|
|
21
21
|
if (value && !["read", "write", "full"].includes(String(value))) {
|
|
@@ -62,11 +62,11 @@ async function run(ctx, args) {
|
|
|
62
62
|
const db = ctx.db();
|
|
63
63
|
const cwd = projectCwd();
|
|
64
64
|
const permission = resolvePermission(ctx);
|
|
65
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, permission, "terminal-swarm") || cwd;
|
|
65
66
|
const swarm = require("../storm/swarm.cjs").create(buildStormDeps(ctx));
|
|
66
67
|
const r = await swarm.cmdSwarm(db, rest, runtimeOverride, {
|
|
67
68
|
cwd,
|
|
68
|
-
|
|
69
|
-
projectPath: cwd,
|
|
69
|
+
projectPath,
|
|
70
70
|
permission,
|
|
71
71
|
});
|
|
72
72
|
return r && r.ok ? 0 : 1;
|
|
@@ -11,7 +11,8 @@ const { runWriteTransaction } = require("../agentlas-sqlite-policy.cjs");
|
|
|
11
11
|
|
|
12
12
|
function run(ctx, args) {
|
|
13
13
|
const ko = ctx.lang === "ko";
|
|
14
|
-
|
|
14
|
+
// 동의 플래그를 슬러그보다 앞에 써도 되도록 첫 번째 비플래그 인자를 대상으로 본다.
|
|
15
|
+
const token = args.find((a) => !String(a).startsWith("-"));
|
|
15
16
|
if (!token) {
|
|
16
17
|
ctx.err(ko ? "사용법: agentlas uninstall <agent>" : "Usage: agentlas uninstall <agent>");
|
|
17
18
|
return 1;
|
|
@@ -45,6 +46,35 @@ function run(ctx, args) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/*
|
|
50
|
+
* 대화 파괴 게이트 — bootstrap-schema.sql:50 chats.agent_id 는
|
|
51
|
+
* `ON DELETE CASCADE`, :61 chat_messages.chat_id 도 CASCADE 다. 즉
|
|
52
|
+
* installed_agents 행 하나를 지우면 그 에이전트의 모든 챗과 메시지가 같이
|
|
53
|
+
* 사라진다(데스크탑과 공유하는 SQLite라 데스크탑 쪽 이력도 함께 증발한다).
|
|
54
|
+
* 그런데 이 명령은 그 사실을 한 줄도 알리지 않고 `✓ 제거됨`만 찍었다 —
|
|
55
|
+
* 되돌릴 수 없는 삭제가 무고지·무확인으로 일어나던 자리다.
|
|
56
|
+
* 그래서 지우기 "전에" 파급 건수를 세고, 대화가 있으면 명시적 동의
|
|
57
|
+
* (--yes/-y/--force) 없이는 아무것도 지우지 않는다. 대화가 0건이면
|
|
58
|
+
* 파괴할 이력이 없으므로 종전대로 그냥 진행한다(불필요한 마찰 금지).
|
|
59
|
+
*/
|
|
60
|
+
const consented = args.some((a) => a === "--yes" || a === "-y" || a === "--force");
|
|
61
|
+
let chatCount = 0;
|
|
62
|
+
let messageCount = 0;
|
|
63
|
+
if (ctx.tableExists(db, "chats")) {
|
|
64
|
+
chatCount = db.prepare("SELECT COUNT(*) AS n FROM chats WHERE agent_id=?").get(agent.id).n;
|
|
65
|
+
if (chatCount && ctx.tableExists(db, "chat_messages")) {
|
|
66
|
+
messageCount = db.prepare(
|
|
67
|
+
"SELECT COUNT(*) AS n FROM chat_messages WHERE chat_id IN (SELECT id FROM chats WHERE agent_id=?)",
|
|
68
|
+
).get(agent.id).n;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (chatCount && !consented) {
|
|
72
|
+
ctx.err(ko
|
|
73
|
+
? `"${agent.slug}" 제거는 대화 ${chatCount}개와 메시지 ${messageCount}개를 함께 영구 삭제합니다 (데스크탑 앱과 공유하는 DB이며 되돌릴 수 없습니다).\n계속하려면: agentlas uninstall ${agent.slug} --yes`
|
|
74
|
+
: `Uninstalling "${agent.slug}" will also permanently delete ${chatCount} chat(s) and ${messageCount} message(s) (shared with the Desktop app; this cannot be undone).\nRe-run to confirm: agentlas uninstall ${agent.slug} --yes`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
let deleted = false;
|
|
49
79
|
runWriteTransaction(db, () => {
|
|
50
80
|
deleted = db.prepare("DELETE FROM installed_agents WHERE id=?").run(agent.id).changes > 0;
|
|
@@ -58,7 +88,11 @@ function run(ctx, args) {
|
|
|
58
88
|
saveRoutes(routes);
|
|
59
89
|
}
|
|
60
90
|
} catch { /* 라우트 정리는 best-effort */ }
|
|
61
|
-
|
|
91
|
+
// 실제로 무엇이 사라졌는지 성공 줄에 남긴다 — 조용한 파괴 금지.
|
|
92
|
+
const cascade = chatCount
|
|
93
|
+
? (ko ? ` (대화 ${chatCount}개 · 메시지 ${messageCount}개 삭제됨)` : ` (deleted ${chatCount} chat(s), ${messageCount} message(s))`)
|
|
94
|
+
: "";
|
|
95
|
+
ctx.out(`${ctx.ui.green("✓")} ${ko ? "제거됨" : "Uninstalled"}: ${agent.slug}${cascade}`);
|
|
62
96
|
return 0;
|
|
63
97
|
}
|
|
64
98
|
ctx.err(ko ? "제거하지 못했습니다." : "Nothing was uninstalled.");
|
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* version — 터미널 버전과 함께 **지금 붙어 있는 Agentlas-OS Core** 를 보고한다.
|
|
4
|
+
*
|
|
5
|
+
* 왜 Core 도 같이 찍나 (2026-07-28):
|
|
6
|
+
* 터미널의 계약 표면(build/upload/workforce/context/…)은 Core 릴리스가 소유한다.
|
|
7
|
+
* Core 는 스스로 갱신되고(agentlas_cloud/update.py), 셸은 그때그때 해석된 루트에
|
|
8
|
+
* 붙는다. 그런데 `version`·`--where`·`doctor` 어디에서도 Core 를 찍지 않아서,
|
|
9
|
+
* 사용자도 지원 담당도 **어떤 Core 위에서 돌고 있는지 알 방법이 없었다**.
|
|
10
|
+
* "최신인데 안 맞는다" 류의 오진이 그 공백에서 나온다. 붙은 대상을 보여주는 것이
|
|
11
|
+
* 진단의 출발점이다.
|
|
12
|
+
*
|
|
13
|
+
* Core 가 없어도 실패하지 않는다 — 56개 명령은 Core 없이 동작한다. 없으면 없다고
|
|
14
|
+
* 정직하게 적는다(조용한 생략 금지).
|
|
15
|
+
*/
|
|
2
16
|
const { readVersion } = require("../agentlas-banner.cjs");
|
|
3
17
|
|
|
18
|
+
function coreLine() {
|
|
19
|
+
try {
|
|
20
|
+
const core = require("../agentlas-core-harness.cjs");
|
|
21
|
+
const root = core.resolveCoreRuntimeRoot();
|
|
22
|
+
if (!root) return "agentlas-os not attached (install: hephaestus update)";
|
|
23
|
+
const version = core.readCoreRuntimeVersion(root);
|
|
24
|
+
// 버전을 못 읽는 루트는 "낡음"이 아니라 "모름"이다. 두 상태를 구분해 적는다 —
|
|
25
|
+
// 모름을 그럴듯한 값으로 메꾸면 그 순간 진단이 거짓이 된다.
|
|
26
|
+
return `agentlas-os ${version || "unknown version"} (${root})`;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return `agentlas-os unavailable (${(error && error.message) || error})`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
4
32
|
function run(ctx) {
|
|
5
33
|
ctx.out(`agentlas ${readVersion()}`);
|
|
34
|
+
ctx.out(coreLine());
|
|
6
35
|
return 0;
|
|
7
36
|
}
|
|
8
37
|
|
|
@@ -11,17 +11,12 @@
|
|
|
11
11
|
* parity().cmdHep(["hep-network", ...]) 호환 탈출구였는데, 그 parity 모듈이 아직
|
|
12
12
|
* v2에 없으므로 정직 정지로 안내만 한다(가짜 성공 금지).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 경로를 만들었다. 그 프로젝트-상태 기계(폴더 방문 기록/활성화 판정)는 아직 v2로
|
|
17
|
-
* 포팅되지 않았다. cmdWorkforce 가 projectPath 를 쓰는 곳은 buildChildEnv 의
|
|
18
|
-
* 프로젝트 dotenv 스코핑뿐이고(agentlas-workforce.cjs 1926행) 부재 시 fail-closed
|
|
19
|
-
* 의존이 없음을 확인했다 — 그래서 여기서는 cwd 를 projectPath 로 넘긴다(같은 폴더
|
|
20
|
-
* 의 .env/.env.local 이 비신뢰 소스로 한 번 더 스캔될 뿐, 권한이 넓어지지 않는다).
|
|
21
|
-
* 프로젝트-상태 모듈이 v2에 착륙하면 이 자리를 그 결과로 교체한다.
|
|
14
|
+
* Write-capable first contact bootstraps the exact current project through the
|
|
15
|
+
* canonical Core merge-only boundary. Read-only inspection remains passive.
|
|
22
16
|
*/
|
|
23
17
|
const { workforceRuntime } = require("../workforce/deps.cjs");
|
|
24
18
|
const { projectCwd } = require("../workforce/capture.cjs");
|
|
19
|
+
const { ensureTerminalProjectForExecutionCli } = require("../project/state.cjs");
|
|
25
20
|
|
|
26
21
|
function resolvePermission(ctx) {
|
|
27
22
|
// v1 resolveTerminalPermission 포팅: 저장된 `full` 은 세션 한정 계약이라
|
|
@@ -70,11 +65,16 @@ async function dispatch(ctx, command, args) {
|
|
|
70
65
|
const db = ctx.db();
|
|
71
66
|
const cwd = projectCwd();
|
|
72
67
|
const permission = resolvePermission(ctx);
|
|
68
|
+
const projectPath = ensureTerminalProjectForExecutionCli(
|
|
69
|
+
db,
|
|
70
|
+
cwd,
|
|
71
|
+
permission,
|
|
72
|
+
`terminal-${command}`,
|
|
73
|
+
) || cwd;
|
|
73
74
|
const runtime = workforceRuntime({ lang: ctx.lang, out: ctx.out });
|
|
74
75
|
const result = await runtime.cmdWorkforce(db, rest, runtimeOverride, {
|
|
75
76
|
cwd,
|
|
76
|
-
|
|
77
|
-
projectPath: cwd,
|
|
77
|
+
projectPath,
|
|
78
78
|
permission,
|
|
79
79
|
});
|
|
80
80
|
return result && result.ok ? 0 : 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
|
};
|
|
@@ -204,13 +204,15 @@ function turnText(res) {
|
|
|
204
204
|
* 회사 1태스크 실행: PLAN → DELEGATE → SYNTHESIZE.
|
|
205
205
|
* @param {object} p
|
|
206
206
|
* db, orch(Orchestrator), firm(firms 행), ceoAgent(rowToAgent 결과),
|
|
207
|
-
* task, runtime,
|
|
207
|
+
* task, runtime(orchestrator), workerRuntime?, resolveWorkerRuntime?(division),
|
|
208
|
+
* permission, cwd, onEvent?({phase,...}),
|
|
208
209
|
* spawnImplFor?({kind:'ceo'|'division', role?}) — 계약 테스트 전용 fake spawn 주입,
|
|
209
210
|
* timeoutConfig? — 테스트 전용.
|
|
210
211
|
* @returns {ok, text, chatId, plan:{text,delegations}, divisions:[{role,name,ok,text,chatId}]}
|
|
211
212
|
*/
|
|
212
213
|
async function runFirmTurn(p) {
|
|
213
214
|
const { db, orch, firm, ceoAgent, task, runtime, permission, cwd } = p;
|
|
215
|
+
const workerRuntime = p.workerRuntime || runtime;
|
|
214
216
|
const onEvent = typeof p.onEvent === "function" ? p.onEvent : () => {};
|
|
215
217
|
const parseDelegations = loadDelegateParser();
|
|
216
218
|
const divisions = resolveDivisions(db, firm, ceoAgent);
|
|
@@ -269,15 +271,20 @@ async function runFirmTurn(p) {
|
|
|
269
271
|
// kind='division' + parent_chat_id(CEO 챗)로 영속된다. 한 본부의 실패는 격리한다.
|
|
270
272
|
onEvent({ phase: "delegate", targets: matched.map((m) => ({ role: m.node.role, name: m.node.name, brief: m.brief })) });
|
|
271
273
|
const divisionResults = await parallelCap(matched, maxParallel(), async (m) => {
|
|
274
|
+
const divisionRuntime = typeof p.resolveWorkerRuntime === "function"
|
|
275
|
+
? p.resolveWorkerRuntime(m.node)
|
|
276
|
+
: workerRuntime;
|
|
272
277
|
const session = orch.spawn({
|
|
273
278
|
agent: m.node.agent,
|
|
274
|
-
runtime,
|
|
279
|
+
runtime: divisionRuntime,
|
|
275
280
|
permission,
|
|
276
281
|
cwd,
|
|
277
282
|
title: `division: ${m.node.role}`,
|
|
278
283
|
parentKey: ceoSession.key,
|
|
279
284
|
activate: false,
|
|
280
|
-
spawnImpl: p.spawnImplFor
|
|
285
|
+
spawnImpl: p.spawnImplFor
|
|
286
|
+
? p.spawnImplFor({ kind: "division", role: m.node.role, runtime: divisionRuntime })
|
|
287
|
+
: undefined,
|
|
281
288
|
timeoutConfig: p.timeoutConfig,
|
|
282
289
|
});
|
|
283
290
|
// 본부 세션도 generic 자동 위임을 끈다 — 터미널 firm 은 아직 tier-3(전문가) 배선이
|
|
@@ -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 {
|