agentlas 1.0.29 → 1.0.36
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 +69 -0
- package/bin/agentlas.cjs +15 -0
- package/engine/agentlas-cloud-runtime.cjs +54 -4
- package/engine/agentlas-input.cjs +10 -1
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +69 -4
- package/engine/agentlas-ui.cjs +2 -0
- package/engine/agentlas-workforce.cjs +28 -5
- package/engine/agentlas.cjs +64 -2
- package/engine/agents/builder.cjs +84 -0
- package/engine/agents/router.cjs +8 -2
- package/engine/automation/launchd.cjs +131 -0
- package/engine/bootstrap-schema.sql +6 -44
- package/engine/browser/cdp.cjs +188 -0
- package/engine/browser/vault.cjs +124 -0
- package/engine/cli-output.cjs +262 -0
- package/engine/cloud-assets/package.cjs +5 -1
- package/engine/commands/automation.cjs +37 -1
- package/engine/commands/browser.cjs +166 -8
- package/engine/commands/build.cjs +101 -20
- package/engine/commands/connect.cjs +162 -9
- package/engine/commands/creds.cjs +16 -2
- package/engine/commands/doctor.cjs +20 -1
- package/engine/commands/document.cjs +79 -0
- package/engine/commands/graph.cjs +50 -10
- package/engine/commands/help.cjs +8 -6
- package/engine/commands/index.cjs +1 -0
- package/engine/commands/list.cjs +10 -1
- package/engine/commands/project.cjs +79 -16
- package/engine/commands/roles.cjs +10 -2
- package/engine/commands/telegram.cjs +23 -16
- package/engine/core/desktop-core-fetch.cjs +98 -0
- package/engine/core/desktop-core.cjs +170 -0
- package/engine/graph/ask-model.cjs +29 -1
- package/engine/graph/interview.cjs +105 -20
- package/engine/graph/layout.cjs +36 -34
- package/engine/graph/vocabulary.generated.cjs +1 -1
- package/engine/hephaestus/runtime.cjs +10 -2
- package/engine/project/controller.cjs +8 -8
- package/engine/project/team.cjs +99 -0
- package/engine/runtime-refusal.cjs +71 -0
- package/engine/runtimes/detect.cjs +3 -0
- package/engine/runtimes/resolve.cjs +1 -1
- package/engine/sessions/session.cjs +15 -2
- package/engine/telegram/connect.cjs +202 -0
- package/engine/ui/palette.cjs +1 -0
- package/engine/ui/repl.cjs +112 -1
- package/engine/vendor/desktop-core.manifest.json +7 -0
- package/engine/workforce/capture.cjs +51 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.34 — 2026-08-06
|
|
4
|
+
|
|
5
|
+
Telegram, standalone. The terminal can now connect a Telegram bot with no desktop app.
|
|
6
|
+
|
|
7
|
+
- `connect telegram <agent|firm>` reads a bot token from stdin (never argv), verifies it
|
|
8
|
+
against api.telegram.org, stores it 0600, polls getUpdates and pairs the first private
|
|
9
|
+
chat, then sends a confirmation.
|
|
10
|
+
- `connect test <id>` / `connect remove <id>` / `connect status`. The connection core is
|
|
11
|
+
plain HTTPS ported from the desktop; no Electron, no plugin, no raw JSON dump.
|
|
12
|
+
|
|
13
|
+
Not yet standalone: auto-creating the bot by piloting BotFather in a browser (the CDP
|
|
14
|
+
browser hardpoint makes this a tractable next layer; it needs a live Telegram web session).
|
|
15
|
+
|
|
16
|
+
## 1.0.33 — 2026-08-06
|
|
17
|
+
|
|
18
|
+
Defect sweep: ran every command with a real prompt and fixed what dumped or stalled.
|
|
19
|
+
|
|
20
|
+
- `connect telegram` / `connect status` no longer dump the raw Hephaestus router JSON —
|
|
21
|
+
they show the local Telegram binding table and honestly note that bot issuance/pairing
|
|
22
|
+
still live in Desktop Connect.
|
|
23
|
+
- `route "<request>"` shows a progress spinner during its ~13s Hub round-trip instead of
|
|
24
|
+
sitting silent.
|
|
25
|
+
|
|
26
|
+
## 1.0.32 — 2026-08-06
|
|
27
|
+
|
|
28
|
+
Standalone: the terminal no longer requires Desktop or the plugin for its core flows.
|
|
29
|
+
(They share artifacts and settings; they are not a prerequisite.)
|
|
30
|
+
|
|
31
|
+
- `project use <agent>` / `project team <agent>…` connect the current folder as a
|
|
32
|
+
project and set an ordered team, entirely from the terminal — so `run "<task>"` and
|
|
33
|
+
plain REPL input work without ever opening Desktop. `project status` shows the team.
|
|
34
|
+
- `build "<request>"` now builds locally: the terminal runtime produces an installable
|
|
35
|
+
agent package (AGENTS.md + manifest.md + README.md) and auto-installs it, instead of
|
|
36
|
+
printing "open the Claude Code / Codex plugin and run /hep-build".
|
|
37
|
+
- Honest-stops in a project-less folder point at `project use`, not "open Desktop".
|
|
38
|
+
|
|
39
|
+
Verified standalone end-to-end: project use → run answered correctly; build produced and
|
|
40
|
+
installed an agent that then ran correctly.
|
|
41
|
+
|
|
42
|
+
## 1.0.31 — 2026-08-06
|
|
43
|
+
|
|
44
|
+
Interactive UX, measured against first-class REPLs with a real PTY.
|
|
45
|
+
|
|
46
|
+
- The empty prompt now guides: a dim ghost hint ("type a task · / commands · @ files
|
|
47
|
+
· ? shortcuts"), `?` prints a shortcuts card, a second empty Enter points the way.
|
|
48
|
+
- Tab-completing a command that takes arguments appends the space, so you can type the
|
|
49
|
+
argument immediately; no-arg commands are unchanged.
|
|
50
|
+
- A plain-language task in a folder with no connected project now says exactly why and how
|
|
51
|
+
to run anyway (localized), instead of a single "recovering…" line that hid the reason.
|
|
52
|
+
Every controller honest-stop carries a machine code so this can never be swallowed again.
|
|
53
|
+
- New PTY-driven gates (repl-guidance, honest-stop-not-swallowed) — these paths live inside
|
|
54
|
+
the readline session and are invisible to spawnSync tests.
|
|
55
|
+
|
|
56
|
+
## 1.0.30 — 2026-08-06
|
|
57
|
+
|
|
58
|
+
CLI-conventions hardening, measured against clig.dev and first-class CLIs.
|
|
59
|
+
|
|
60
|
+
- A closed pipe is a reader saying stop: `agentlas … | head` no longer crashes with EPIPE.
|
|
61
|
+
- Unexpected crashes now print a one-line summary and a pre-filled GitHub issue URL, not just a raw stack.
|
|
62
|
+
- `TERM=dumb` disables ANSI colors (editor shells, some CI).
|
|
63
|
+
- Secrets stop landing in shell history: `creds save --value -` reads the secret from stdin;
|
|
64
|
+
passing it via argv now prints an exposure notice.
|
|
65
|
+
- `list --json`, `doctor --json`, `roles --json` — machine contracts for scripts.
|
|
66
|
+
- Workforce staffing survives transient API failures: a typed transient error
|
|
67
|
+
(connection closed mid-response, timeouts, overload) on a no-authority/read-only stage
|
|
68
|
+
is retried exactly once; write-capable stages are never retried. A 40-minute, 2.1M-token
|
|
69
|
+
live run previously died on the final re-verification call for exactly this.
|
|
70
|
+
- New gate: clig-conventions-contract; retry contract added to the workforce runtime gate.
|
|
71
|
+
|
|
3
72
|
## 1.0.29 — 2026-08-05
|
|
4
73
|
|
|
5
74
|
Terminal-wide audit release. Every command was executed for real; what follows fixes what that audit found.
|
package/bin/agentlas.cjs
CHANGED
|
@@ -18,6 +18,21 @@
|
|
|
18
18
|
*/
|
|
19
19
|
"use strict";
|
|
20
20
|
|
|
21
|
+
/*
|
|
22
|
+
* EPIPE 방어 — 발행본 실설치 검증에서 발견(2026-08-06): `agentlas version | head -1`
|
|
23
|
+
* 처럼 파이프 소비자가 먼저 닫히면 stdout write가 EPIPE를 던져 스택트레이스로
|
|
24
|
+
* 크래시했다. `| head` `| less` `| grep -m1`은 CLI의 일상 사용 패턴이다.
|
|
25
|
+
* 파이프 단절은 오류가 아니라 "그만 읽겠다"는 신호 — 조용히 성공 종료한다.
|
|
26
|
+
* (시나리오 게이트가 못 잡았던 이유: spawnSync는 파이프를 끝까지 읽는다.
|
|
27
|
+
* test/user-scenarios-contract.cjs 의 파이프 시나리오가 이 계약을 잠근다.)
|
|
28
|
+
*/
|
|
29
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
30
|
+
stream.on("error", (error) => {
|
|
31
|
+
if (error && error.code === "EPIPE") process.exit(0);
|
|
32
|
+
throw error;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
const fs = require("node:fs");
|
|
22
37
|
const os = require("node:os");
|
|
23
38
|
const path = require("node:path");
|
|
@@ -38,11 +38,56 @@ function collectFiles(root) {
|
|
|
38
38
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Local sourcePackage identity — the wizard manifest's `packageHash`.
|
|
42
|
+
//
|
|
43
|
+
// This is NOT the delivered Agent Cloud artifact hash. That one is
|
|
44
|
+
// `path-sha256-executable-v2` and lives in engine/cloud-assets/package.cjs,
|
|
45
|
+
// byte-identical with Desktop's hashPackage and Agentlas-OS upload.py. The two
|
|
46
|
+
// are separate contracts on purpose (upload.py: "distinct from agentlas.json's
|
|
47
|
+
// local sourcePackage hash contract").
|
|
48
|
+
//
|
|
49
|
+
// The canonical definition of THIS hash is the kernel's
|
|
50
|
+
// `canonical_package_hash_hex` in Agentlas-OS/agentlas_cloud/runtime.py. This
|
|
51
|
+
// file mirrors it and must produce the same digest for the same folder; the
|
|
52
|
+
// two used to disagree on every input because this copy had no version prefix
|
|
53
|
+
// and skipped only agentlas.json. Keep the three constants below in step with
|
|
54
|
+
// the kernel: PACKAGE_HASH_VERSION, PACKAGE_HASH_EXCLUDED_PATHS, and the
|
|
55
|
+
// experience-lineage rule.
|
|
56
|
+
const PACKAGE_HASH_VERSION = "agentlas-package-hash/v2";
|
|
57
|
+
const LOCAL_EXPERIENCE_LINEAGE_PATH = ".agentlas/experience-relations.jsonl";
|
|
58
|
+
const PACKAGE_HASH_EXCLUDED_PATHS = new Set([
|
|
59
|
+
"agentlas.json",
|
|
60
|
+
".agentlas/brief.json",
|
|
61
|
+
".agentlas/security-scan.json",
|
|
62
|
+
".agentlas/security-llm-judgment.json",
|
|
63
|
+
".agentlas/field-test-report.json",
|
|
64
|
+
LOCAL_EXPERIENCE_LINEAGE_PATH,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function isLocalExperienceLineagePath(filePath) {
|
|
68
|
+
const normalized = String(filePath || "").replaceAll("\\", "/");
|
|
69
|
+
return (
|
|
70
|
+
normalized === LOCAL_EXPERIENCE_LINEAGE_PATH ||
|
|
71
|
+
normalized.startsWith(`${LOCAL_EXPERIENCE_LINEAGE_PATH}.`) ||
|
|
72
|
+
normalized.startsWith(".agentlas/.experience-relations.jsonl.")
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function packageHashIncludes(filePath) {
|
|
77
|
+
const normalized = String(filePath || "").replaceAll("\\", "/");
|
|
78
|
+
return !PACKAGE_HASH_EXCLUDED_PATHS.has(normalized) && !isLocalExperienceLineagePath(normalized);
|
|
79
|
+
}
|
|
80
|
+
|
|
41
81
|
function hashPackage(files) {
|
|
42
82
|
const h = crypto.createHash("sha256");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
83
|
+
h.update(PACKAGE_HASH_VERSION);
|
|
84
|
+
h.update("\0");
|
|
85
|
+
// Codepoint order, not localeCompare: the kernel sorts by raw string order.
|
|
86
|
+
const ordered = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
87
|
+
for (const file of ordered) {
|
|
88
|
+
const normalized = String(file.path || "").replaceAll("\\", "/");
|
|
89
|
+
if (!packageHashIncludes(normalized)) continue;
|
|
90
|
+
h.update(normalized);
|
|
46
91
|
h.update("\0");
|
|
47
92
|
h.update(file.content);
|
|
48
93
|
h.update("\0");
|
|
@@ -55,9 +100,14 @@ function inferEntry(files) {
|
|
|
55
100
|
return ["AGENTS.md", "agent.md", "CLAUDE.md", "README.md"].find((candidate) => paths.has(candidate)) || files[0]?.path || "AGENTS.md";
|
|
56
101
|
}
|
|
57
102
|
|
|
103
|
+
// Skills actually present in the package. An empty result stays empty.
|
|
104
|
+
// A package with no skills used to be given a literal placeholder skill id.
|
|
105
|
+
// Once skills live outside the core that placeholder fires on every modular
|
|
106
|
+
// agent and fills the Workforce index with a skill nobody has.
|
|
107
|
+
// An absent value is an empty list, never a stand-in.
|
|
58
108
|
function inferSkills(files) {
|
|
59
109
|
const skills = files.map((file) => file.path.match(/(?:^|\/)skills\/([^/]+)\/SKILL\.md$/)?.[1]).filter(Boolean);
|
|
60
|
-
return [...new Set(skills)].sort()
|
|
110
|
+
return [...new Set(skills)].sort();
|
|
61
111
|
}
|
|
62
112
|
|
|
63
113
|
function buildManifest(root, options = {}) {
|
|
@@ -704,7 +704,16 @@ function attachSlashPalette(rl, opts = {}) {
|
|
|
704
704
|
const list = rows();
|
|
705
705
|
if (!list.length) return false;
|
|
706
706
|
if (state.selected < 0 || state.selected >= list.length) state.selected = 0;
|
|
707
|
-
|
|
707
|
+
const row = list[state.selected];
|
|
708
|
+
/*
|
|
709
|
+
* 인자를 받는 명령은 확정 즉시 스페이스까지 넣는다 (2026-08-06, 레퍼런스
|
|
710
|
+
* 대조): Tab 후 바로 인자를 타이핑하게 — 스페이스를 손으로 넣는 한 박자가
|
|
711
|
+
* 모든 인자형 명령에서 반복되는 마찰이었다. 인자 없는 명령(/help 등)은
|
|
712
|
+
* 그대로 — Enter 로 즉시 실행하는 흐름을 깨지 않는다.
|
|
713
|
+
*/
|
|
714
|
+
const takesArgs = Boolean(String(row.usage || row.args || "").trim())
|
|
715
|
+
&& String(row.usage || `${row.command} ${row.args || ""}`).trim() !== row.command;
|
|
716
|
+
state.selectedCommand = row.command + (takesArgs ? " " : "");
|
|
708
717
|
replaceLine(state.selectedCommand);
|
|
709
718
|
clear();
|
|
710
719
|
return true;
|
|
Binary file
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* 스키마는 실측으로 확인됨 (cli/agentlas.cjs 상단 주석 참고).
|
|
14
14
|
*/
|
|
15
15
|
const { execFileSync, spawn } = require("node:child_process");
|
|
16
|
+
const { detectRuntimeRefusal } = require("./runtime-refusal.cjs");
|
|
16
17
|
const crypto = require("node:crypto");
|
|
17
18
|
const fs = require("node:fs");
|
|
18
19
|
const os = require("node:os");
|
|
@@ -421,11 +422,26 @@ function handleClaudeLine(line, st, ui) {
|
|
|
421
422
|
cost_usd: obj.total_cost_usd,
|
|
422
423
|
duration_ms: obj.duration_ms,
|
|
423
424
|
};
|
|
424
|
-
if (obj.is_error)
|
|
425
|
+
if (obj.is_error) {
|
|
426
|
+
st.error = obj.result || "claude error";
|
|
427
|
+
// api_error_status:429·error:"rate_limit"이 실려 오는 실측 형태 — 한도는 quota.
|
|
428
|
+
st.errorKind = obj.api_error_status === 429 || obj.terminal_reason === "api_error"
|
|
429
|
+
? "quota" : "exit";
|
|
430
|
+
st.errorSource = "marker";
|
|
431
|
+
}
|
|
425
432
|
return;
|
|
426
433
|
case "rate_limit_event":
|
|
434
|
+
/*
|
|
435
|
+
* ★한도 거절은 **표식이다** — 경고만 찍고 버리면 안 된다.
|
|
436
|
+
* 실측(2026-08-06): 이 이벤트가 왔는데도 error가 비어 있어, 뒤따르는 거절문
|
|
437
|
+
* ("You've hit your weekly limit")이 정상 답으로 세어졌다. 살아 있는 다른
|
|
438
|
+
* 런타임으로 폴백이 안 걸린 진범. status:rejected면 그 자체로 실패다.
|
|
439
|
+
*/
|
|
427
440
|
if (obj.rate_limit_info && obj.rate_limit_info.status === "rejected") {
|
|
428
441
|
ui.warn(uiText(ui, "runtime.rateLimit", "Claude"));
|
|
442
|
+
if (!st.error) st.error = "claude rate limit rejected";
|
|
443
|
+
st.errorKind = "quota";
|
|
444
|
+
st.errorSource = "marker";
|
|
429
445
|
}
|
|
430
446
|
return;
|
|
431
447
|
default:
|
|
@@ -501,6 +517,8 @@ function handleCodexLine(line, st, ui) {
|
|
|
501
517
|
case "turn.failed":
|
|
502
518
|
case "error":
|
|
503
519
|
st.error = (obj.error && (obj.error.message || obj.error)) || "codex error";
|
|
520
|
+
st.errorKind = "exit";
|
|
521
|
+
st.errorSource = "marker";
|
|
504
522
|
ui.error(String(st.error));
|
|
505
523
|
st.errorShown = true;
|
|
506
524
|
return;
|
|
@@ -756,9 +774,24 @@ function handleGeminiLine(line, st, ui) {
|
|
|
756
774
|
duration_ms: s.duration_ms,
|
|
757
775
|
};
|
|
758
776
|
st.finalText = st.text;
|
|
759
|
-
|
|
777
|
+
// error 이벤트가 이미 구체 사유를 남겼으면 뭉뚱그린 상태 문자열로 덮지 않는다.
|
|
778
|
+
if (obj.status && obj.status !== "success" && !st.error) {
|
|
779
|
+
st.error = `gemini ${obj.status}`;
|
|
780
|
+
st.errorKind = "exit";
|
|
781
|
+
st.errorSource = "marker";
|
|
782
|
+
}
|
|
760
783
|
return;
|
|
761
784
|
}
|
|
785
|
+
case "error":
|
|
786
|
+
/*
|
|
787
|
+
* ★프로토콜이 선언하는 이벤트인데 핸들러가 없어 조용히 버려졌다
|
|
788
|
+
* (capture.cjs는 목록에 적어 두고도 안 읽었다). 인증 실패("IneligibleTierError")
|
|
789
|
+
* 같은 것이 여기로 온다 — 버리면 빈 성공이 된다.
|
|
790
|
+
*/
|
|
791
|
+
st.error = (obj.error && (obj.error.message || obj.error)) || "gemini error";
|
|
792
|
+
st.errorKind = /auth|login|credential|tier|eligib/i.test(String(st.error)) ? "auth" : "exit";
|
|
793
|
+
st.errorSource = "marker";
|
|
794
|
+
return;
|
|
762
795
|
default:
|
|
763
796
|
return;
|
|
764
797
|
}
|
|
@@ -806,8 +839,20 @@ function runNativeTurn(req) {
|
|
|
806
839
|
} else if (kind === "gemini") {
|
|
807
840
|
args = geminiArgs(launchReq);
|
|
808
841
|
lineHandler = (l) => handleGeminiLine(l, st, ui);
|
|
842
|
+
} else if (kind === "agy") {
|
|
843
|
+
/*
|
|
844
|
+
* Antigravity CLI — stream-json이 없다(실측 1.1.10). 평문 stdout을 그대로 텍스트로.
|
|
845
|
+
* ★--prompt는 값 플래그가 아니라 --print의 별칭이다(실측: 프롬프트가 조용히 유실됐던
|
|
846
|
+
* 사고) — 프롬프트는 반드시 위치 인자로 넘긴다. 시스템 프롬프트는 본문에 앞세운다.
|
|
847
|
+
*/
|
|
848
|
+
const agyPrompt = launchReq.systemPrompt
|
|
849
|
+
? `${launchReq.systemPrompt}\n\n---\n\n${launchReq.prompt}`
|
|
850
|
+
: launchReq.prompt;
|
|
851
|
+
args = ["--print", agyPrompt, ...(launchReq.model ? ["--model", launchReq.model] : [])];
|
|
852
|
+
plainStream = true;
|
|
853
|
+
lineHandler = null;
|
|
809
854
|
} else {
|
|
810
|
-
return Promise.resolve({ text: "", session: st.session, error: `unknown runtime: ${kind}
|
|
855
|
+
return Promise.resolve({ text: "", session: st.session, error: `unknown runtime: ${kind}`, errorKind: "unsupported", errorSource: "marker" });
|
|
811
856
|
}
|
|
812
857
|
} catch (error) {
|
|
813
858
|
const message = error && error.message ? error.message : String(error);
|
|
@@ -883,6 +928,8 @@ function runNativeTurn(req) {
|
|
|
883
928
|
session: st.session,
|
|
884
929
|
usage: st.usage,
|
|
885
930
|
error: termination ? termination.message : "native runtime stopped",
|
|
931
|
+
errorKind: "timeout",
|
|
932
|
+
errorSource: "marker",
|
|
886
933
|
terminationVerified: Boolean(termination?.verified),
|
|
887
934
|
});
|
|
888
935
|
};
|
|
@@ -976,13 +1023,31 @@ function runNativeTurn(req) {
|
|
|
976
1023
|
ui.stopSpinner();
|
|
977
1024
|
const text = (st.finalText || st.text || "").trim();
|
|
978
1025
|
if (st.usage) ui.cost(st.usage);
|
|
1026
|
+
/*
|
|
1027
|
+
* ★표식 없이 완주했는데 산출물이 거절 고지문인 경우 — 실측: codex 한도는
|
|
1028
|
+
* 거절문이 agent_message로 오고 turn.completed로 끝난다(표식 0, exit 0).
|
|
1029
|
+
* 이 한 자리에서만 텍스트 판별을 허용하고, 출처를 heuristic으로 남긴다
|
|
1030
|
+
* (판별 규칙은 runtime-refusal.cjs 한 곳 — 여기서 정규식을 다시 쓰지 않는다).
|
|
1031
|
+
*/
|
|
1032
|
+
if (code === 0 && !st.error) {
|
|
1033
|
+
const refusal = detectRuntimeRefusal(text);
|
|
1034
|
+
if (refusal) {
|
|
1035
|
+
st.error = refusal.message;
|
|
1036
|
+
st.errorKind = refusal.kind;
|
|
1037
|
+
st.errorSource = "heuristic";
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
979
1040
|
const exitError = code === 0
|
|
980
1041
|
? st.error
|
|
981
1042
|
: st.error || [
|
|
982
1043
|
`${kind} exited with code ${code == null ? "unknown" : code}`,
|
|
983
1044
|
stripAnsi(stderrBuf).slice(-4000),
|
|
984
1045
|
].filter(Boolean).join("\n");
|
|
985
|
-
|
|
1046
|
+
if (exitError && !st.errorKind) { st.errorKind = "exit"; st.errorSource = st.errorSource || "exit"; }
|
|
1047
|
+
finish({
|
|
1048
|
+
text, session: st.session, usage: st.usage, error: exitError,
|
|
1049
|
+
...(exitError ? { errorKind: st.errorKind, errorSource: st.errorSource } : {}),
|
|
1050
|
+
});
|
|
986
1051
|
});
|
|
987
1052
|
|
|
988
1053
|
armIdle();
|
package/engine/agentlas-ui.cjs
CHANGED
|
@@ -15,6 +15,8 @@ const RESET = "\x1b[0m";
|
|
|
15
15
|
|
|
16
16
|
function colorEnabled() {
|
|
17
17
|
if (process.env.NO_COLOR != null && process.env.NO_COLOR !== "") return false;
|
|
18
|
+
// clig.dev: TERM=dumb 인 터미널(에디터 내장 셸, 일부 CI)은 ANSI를 렌더하지 못한다.
|
|
19
|
+
if (process.env.TERM === "dumb") return false;
|
|
18
20
|
if (process.env.FORCE_COLOR === "1" || process.env.FORCE_COLOR === "true") return true;
|
|
19
21
|
if (process.env.AGENTLAS_NO_COLOR === "1") return false;
|
|
20
22
|
return !!process.stdout.isTTY;
|
|
@@ -2120,6 +2120,28 @@ function create(deps = {}) {
|
|
|
2120
2120
|
tokenLedger.push({ role: role || "stage", runtime: runtimeKind || "?", model: modelPin || null, input, output, cached });
|
|
2121
2121
|
}
|
|
2122
2122
|
|
|
2123
|
+
/*
|
|
2124
|
+
* 일시 API 오류 1회 재시도 (실측 2026-08-05: 4슬롯·2.1M 토큰 편성이 마지막
|
|
2125
|
+
* 재검증 호출의 "Connection closed mid-response" 하나로 전멸).
|
|
2126
|
+
* 재시도의 근거는 부수효과 부재가 아니라 **일시 오류의 기계 표식**이고,
|
|
2127
|
+
* 부수효과가 가능한 write 권한 단계는 표식이 있어도 재시도하지 않는다
|
|
2128
|
+
* (자동화 스케줄러와 같은 원칙). D.runModel 주입·CLI 캡처·API 백엔드
|
|
2129
|
+
* 세 경로 모두 이 관문을 지난다 — 하니스가 단위로 검증할 수 있는 이유.
|
|
2130
|
+
*/
|
|
2131
|
+
// 한도·429는 재시도 분류에 들어가야 한다 — 빠져 있으면 같은 막힌 런타임에 즉시 재도전만 한다.
|
|
2132
|
+
const TRANSIENT_MODEL_ERROR_RE = /Connection closed mid-response|"terminal_reason":"api_error"|ECONNRESET|ETIMEDOUT|socket hang up|overloaded_error|rate.?limit|quota|\b429\b|usage limit|weekly limit/i;
|
|
2133
|
+
async function withTransientModelRetry(authorityMode, invoke) {
|
|
2134
|
+
try {
|
|
2135
|
+
return await invoke();
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
const replaySafeAuthority = authorityMode === "no-authority" || authorityMode === "read-only";
|
|
2138
|
+
const transient = TRANSIENT_MODEL_ERROR_RE.test(String((error && error.message) || error));
|
|
2139
|
+
if (!replaySafeAuthority || !transient) throw error;
|
|
2140
|
+
process.stderr.write(`workforce: transient api error on a ${authorityMode} stage — retrying once\n`);
|
|
2141
|
+
return invoke();
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2123
2145
|
async function runModel(runtime, system, prompt, context) {
|
|
2124
2146
|
const invocation = stageInvocation(runtime, context);
|
|
2125
2147
|
const executionRuntime = invocation.executionRuntime;
|
|
@@ -2134,7 +2156,7 @@ function create(deps = {}) {
|
|
|
2134
2156
|
? `${system}\n\n${localContextSlice}`
|
|
2135
2157
|
: system;
|
|
2136
2158
|
if (typeof D.runModel === "function") {
|
|
2137
|
-
return normalizeModelResult(await D.runModel({
|
|
2159
|
+
return withTransientModelRetry(context.authorityMode || "no-authority", async () => normalizeModelResult(await D.runModel({
|
|
2138
2160
|
runtime: executionRuntime,
|
|
2139
2161
|
system: effectiveSystem,
|
|
2140
2162
|
prompt,
|
|
@@ -2145,7 +2167,7 @@ function create(deps = {}) {
|
|
|
2145
2167
|
modelPin: invocation.modelPin,
|
|
2146
2168
|
effortPin: invocation.effort,
|
|
2147
2169
|
},
|
|
2148
|
-
}));
|
|
2170
|
+
})));
|
|
2149
2171
|
}
|
|
2150
2172
|
if (executionRuntime.mode === "cli") {
|
|
2151
2173
|
const authorityMode = context.authorityMode || "no-authority";
|
|
@@ -2174,7 +2196,7 @@ function create(deps = {}) {
|
|
|
2174
2196
|
}
|
|
2175
2197
|
noteIsolationWeakness(executionRuntime.kind, invocation.role);
|
|
2176
2198
|
}
|
|
2177
|
-
const
|
|
2199
|
+
const captureOnce = async () => normalizeModelResult(await D.captureRuntime(executionRuntime.kind, effectiveSystem, prompt, {
|
|
2178
2200
|
cwd: context.cwd,
|
|
2179
2201
|
env: context.env,
|
|
2180
2202
|
permission: context.permission,
|
|
@@ -2187,16 +2209,17 @@ function create(deps = {}) {
|
|
|
2187
2209
|
outputLimitBytes: authorityMode === "read-only" ? 24 * 1024 * 1024 : undefined,
|
|
2188
2210
|
envelope: true,
|
|
2189
2211
|
}));
|
|
2212
|
+
const captured = await withTransientModelRetry(authorityMode, captureOnce);
|
|
2190
2213
|
recordStageTokens(invocation.role, executionRuntime.kind, invocation.modelPin, captured.usage);
|
|
2191
2214
|
return captured;
|
|
2192
2215
|
}
|
|
2193
|
-
const viaApi = normalizeModelResult(await D.runApi(
|
|
2216
|
+
const viaApi = await withTransientModelRetry(context.authorityMode || "no-authority", async () => normalizeModelResult(await D.runApi(
|
|
2194
2217
|
executionRuntime.backend,
|
|
2195
2218
|
invocation.modelPin,
|
|
2196
2219
|
effectiveSystem,
|
|
2197
2220
|
prompt,
|
|
2198
2221
|
{ effort: invocation.effort, envelope: true },
|
|
2199
|
-
));
|
|
2222
|
+
)));
|
|
2200
2223
|
recordStageTokens(invocation.role, executionRuntime.backend, invocation.modelPin, viaApi.usage);
|
|
2201
2224
|
return viaApi;
|
|
2202
2225
|
}
|
package/engine/agentlas.cjs
CHANGED
|
@@ -14,10 +14,51 @@
|
|
|
14
14
|
* ctx.out/err stdout/stderr 한 줄 출력
|
|
15
15
|
* ctx.tableExists / ctx.columnExists
|
|
16
16
|
*/
|
|
17
|
+
/*
|
|
18
|
+
* EPIPE 방어 — 발행본 1.0.29 실설치 검증에서 발견(2026-08-06):
|
|
19
|
+
* `agentlas version | head -1` 처럼 파이프 소비자가 먼저 닫히면 stdout write가
|
|
20
|
+
* EPIPE를 던져 스택트레이스로 크래시했다(실측: version.cjs → ctx.out → EPIPE).
|
|
21
|
+
* `| head` `| less` `| grep -m1` 은 CLI의 일상 사용 패턴이고, 파이프 단절은
|
|
22
|
+
* 오류가 아니라 "그만 읽겠다"는 신호다 — 조용히 성공 종료한다. 런처(bin)는
|
|
23
|
+
* 엔진을 stdio:"inherit" 로 spawn 하므로 이 방어는 엔진 프로세스에 있어야 한다.
|
|
24
|
+
*/
|
|
25
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
26
|
+
stream.on("error", (error) => {
|
|
27
|
+
if (error && error.code === "EPIPE") process.exit(0);
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* 예상 못 한 크래시의 마지막 예의 (clig.dev: unexpected error에는 디버그 정보와
|
|
34
|
+
* 버그 리포트 경로를, 리포트는 미리 채워진 URL로 손쉽게).
|
|
35
|
+
* 예전에는 Node 기본 동작(원시 스택트레이스)이 그대로 사용자에게 쏟아졌다 —
|
|
36
|
+
* EPIPE 크래시(1.0.29 실측)가 정확히 그 모습이었다. 스택은 진단에 필요하므로
|
|
37
|
+
* 숨기지 않되, 한 줄 요약과 이슈 URL(제목 미리 채움)을 함께 준다.
|
|
38
|
+
* 종료 코드는 관례대로 1. 여기서 복구를 시도하지 않는다(crash-only).
|
|
39
|
+
*/
|
|
40
|
+
function reportCrash(kind, error) {
|
|
41
|
+
const message = String((error && error.stack) || error);
|
|
42
|
+
const title = encodeURIComponent(`crash: ${String((error && error.message) || error).slice(0, 100)}`);
|
|
43
|
+
process.stderr.write([
|
|
44
|
+
"",
|
|
45
|
+
`agentlas hit an unexpected error (${kind}).`,
|
|
46
|
+
message,
|
|
47
|
+
"",
|
|
48
|
+
`Report it (pre-filled): https://github.com/agentlas-ai/agentlas-terminal/issues/new?title=${title}`,
|
|
49
|
+
`Include: your command, agentlas ${(() => { try { return require("./agentlas-banner.cjs").readVersion(); } catch { return "?"; } })()}, node ${process.version}, ${process.platform}.`,
|
|
50
|
+
"",
|
|
51
|
+
].join("\n"));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
process.on("uncaughtException", (error) => reportCrash("uncaughtException", error));
|
|
55
|
+
process.on("unhandledRejection", (error) => reportCrash("unhandledRejection", error));
|
|
56
|
+
|
|
17
57
|
const { openDb, seedBuiltins, tableExists, columnExists } = require("./core/db.cjs");
|
|
18
58
|
const { userDataDir } = require("./core/paths.cjs");
|
|
19
59
|
const { loadPrefs } = require("./agentlas-config.cjs");
|
|
20
60
|
const { Ui } = require("./agentlas-ui.cjs");
|
|
61
|
+
const { parseOutputFlags, render, renderError, isRichUi } = require("./cli-output.cjs");
|
|
21
62
|
const commands = require("./commands/index.cjs");
|
|
22
63
|
|
|
23
64
|
const SUPPORTED_LANGS = new Set(["ko", "en"]);
|
|
@@ -61,6 +102,22 @@ function buildCtx() {
|
|
|
61
102
|
},
|
|
62
103
|
out: (s = "") => process.stdout.write(s + "\n"),
|
|
63
104
|
err: (s = "") => process.stderr.write(s + "\n"),
|
|
105
|
+
/*
|
|
106
|
+
* 출력 계약 — 명령은 문자열이 아니라 {데이터+스키마}를 준다(cli-output.cjs).
|
|
107
|
+
* 형식(--json/--yaml/--quiet/--no-headers/--no-color) 해석은 여기 한 곳이라,
|
|
108
|
+
* 명령마다 --json 유무가 갈리거나 에러 형식이 달라지는 일이 없다.
|
|
109
|
+
*/
|
|
110
|
+
output: { ...require("./cli-output.cjs").DEFAULT_OPTIONS },
|
|
111
|
+
emit(result) {
|
|
112
|
+
const text = render(result, this.output);
|
|
113
|
+
if (text) process.stdout.write(text + "\n");
|
|
114
|
+
},
|
|
115
|
+
fail(error) {
|
|
116
|
+
process.stderr.write(renderError(error, this.output) + "\n");
|
|
117
|
+
},
|
|
118
|
+
get richUi() {
|
|
119
|
+
return isRichUi(this.output);
|
|
120
|
+
},
|
|
64
121
|
db: () => {
|
|
65
122
|
if (_db) return _db;
|
|
66
123
|
_db = openDb();
|
|
@@ -123,12 +180,17 @@ function main() {
|
|
|
123
180
|
return a;
|
|
124
181
|
});
|
|
125
182
|
|
|
183
|
+
// 전역 출력 플래그는 명령에 닿기 전에 한 곳에서 뜯어낸다 —
|
|
184
|
+
// 명령마다 --json 유무가 갈리던 것을 구조로 막는다.
|
|
185
|
+
const { options: outputOptions, rest: commandArgv } = parseOutputFlags(normalized);
|
|
126
186
|
const ctx = buildCtx();
|
|
187
|
+
ctx.output = outputOptions;
|
|
127
188
|
let code;
|
|
128
189
|
try {
|
|
129
|
-
code = commands.dispatch(ctx,
|
|
190
|
+
code = commands.dispatch(ctx, commandArgv);
|
|
130
191
|
} catch (e) {
|
|
131
|
-
|
|
192
|
+
// 에러도 같은 형식 규율을 따른다: --json 이면 {"error":{code,message}}.
|
|
193
|
+
ctx.fail(e);
|
|
132
194
|
process.exit(1);
|
|
133
195
|
}
|
|
134
196
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* agents/builder — 터미널 소유 로컬 에이전트 빌더 (독립).
|
|
4
|
+
*
|
|
5
|
+
* 배경(2026-08-06 오너 원칙): 데스크탑/플러그인은 산출물·설정을 공유할 뿐 선행
|
|
6
|
+
* 전제가 아니다. 그런데 `agentlas build "<req>"`는 Hephaestus 네이티브
|
|
7
|
+
* 패스스루라 "Open Claude Code or Codex with the plugin, then /hep-build"라는
|
|
8
|
+
* 스텁만 냈다 — 플러그인 강제. 터미널은 자체 런타임(claude-code/codex/gemini)이
|
|
9
|
+
* 있으므로 빌더를 로컬로 돌린다.
|
|
10
|
+
*
|
|
11
|
+
* 계약: 빌더 에이전트를 멱등 시드(installed_agents)하고, `run`과 같은 실행
|
|
12
|
+
* 인프라(Orchestrator 세션)로 돌린다. 산출물은 `import`가 읽는 로컬 폴더 형식
|
|
13
|
+
* (AGENTS.md=시스템 프롬프트, manifest.md=이름/태그라인). 빌더가 마지막 줄에
|
|
14
|
+
* `BUILT: <folder>`를 찍으면 그 폴더를 자동 import 한다. 실패해도 폴더는 남아
|
|
15
|
+
* `agentlas import <folder>`로 언제든 설치할 수 있다.
|
|
16
|
+
*/
|
|
17
|
+
const crypto = require("node:crypto");
|
|
18
|
+
const { runWriteTransaction } = require("../agentlas-sqlite-policy.cjs");
|
|
19
|
+
const { columnExists } = require("../core/db.cjs");
|
|
20
|
+
|
|
21
|
+
const BUILDER_SLUG = "agentlas-builder";
|
|
22
|
+
const BUILDER_ID = "builtin-agentlas-builder";
|
|
23
|
+
|
|
24
|
+
const BUILDER_SYSTEM_PROMPT = [
|
|
25
|
+
"You are the Agentlas local agent builder, running inside the Agentlas terminal.",
|
|
26
|
+
"Your job: turn the user's request into an installable Agentlas agent, entirely on this machine — no external plugin, no desktop app.",
|
|
27
|
+
"",
|
|
28
|
+
"Produce a folder the terminal can import. In the current working directory create a folder named after the agent (kebab-case slug), containing exactly:",
|
|
29
|
+
" - AGENTS.md — the agent's full system prompt / soul: who it is, what it does, how it behaves, its guardrails. Write it as the instructions the agent itself will run under. Be specific and production-ready, not a description of the agent.",
|
|
30
|
+
" - manifest.md — first line `# <Agent Name>`, second line a one-sentence tagline.",
|
|
31
|
+
" - README.md — a short human-facing summary of what the agent does and how to use it.",
|
|
32
|
+
"",
|
|
33
|
+
"Rules:",
|
|
34
|
+
" - Decide the agent's scope from the request; if the request is thin, choose sensible, specific defaults and state them in README.md rather than asking endless questions.",
|
|
35
|
+
" - Do not invent credentials, API keys, or secrets. If the agent needs an env var, name it in README.md as something the user provides later.",
|
|
36
|
+
" - Keep everything inside the new folder. Do not modify files outside it.",
|
|
37
|
+
" - When finished, print exactly one final line: `BUILT: <relative-folder-path>` so the terminal can install it.",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
/** 빌더 에이전트를 멱등 보장한다(installed_agents). 반환: 에이전트 행 형태. */
|
|
41
|
+
function ensureBuilderAgent(db) {
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const hasVisibility = columnExists(db, "installed_agents", "visibility");
|
|
44
|
+
runWriteTransaction(db, () => {
|
|
45
|
+
const existing = db.prepare("SELECT id FROM installed_agents WHERE id=? OR slug=?").get(BUILDER_ID, BUILDER_SLUG);
|
|
46
|
+
if (existing) {
|
|
47
|
+
db.prepare("UPDATE installed_agents SET system_prompt=?, name=?, name_en=?, tagline=?, tagline_en=? WHERE id=?")
|
|
48
|
+
.run(BUILDER_SYSTEM_PROMPT, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", existing.id);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (hasVisibility) {
|
|
52
|
+
db.prepare(
|
|
53
|
+
"INSERT INTO installed_agents (id, slug, name, name_en, tagline, tagline_en, system_prompt, mcp_servers_json, env_requirements_json, preferred_backend, trust_grade, installed_at, tone, builtin, role, visibility) " +
|
|
54
|
+
"VALUES (?,?,?,?,?,?,?,'[]','[]',NULL,'A',?,?,1,?,?)",
|
|
55
|
+
).run(BUILDER_ID, BUILDER_SLUG, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", BUILDER_SYSTEM_PROMPT, now, "blue", "orchestrator", "visible");
|
|
56
|
+
} else {
|
|
57
|
+
db.prepare(
|
|
58
|
+
"INSERT INTO installed_agents (id, slug, name, name_en, tagline, tagline_en, system_prompt, mcp_servers_json, env_requirements_json, preferred_backend, trust_grade, installed_at, tone, builtin, role) " +
|
|
59
|
+
"VALUES (?,?,?,?,?,?,?,'[]','[]',NULL,'A',?,?,1,?)",
|
|
60
|
+
).run(BUILDER_ID, BUILDER_SLUG, "Agent Builder", "Agent Builder", "Builds Agentlas agents locally", "Builds Agentlas agents locally", BUILDER_SYSTEM_PROMPT, now, "blue", "orchestrator");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
id: BUILDER_ID,
|
|
65
|
+
slug: BUILDER_SLUG,
|
|
66
|
+
name: "Agent Builder",
|
|
67
|
+
nameEn: "Agent Builder",
|
|
68
|
+
systemPrompt: BUILDER_SYSTEM_PROMPT,
|
|
69
|
+
builtin: true,
|
|
70
|
+
role: "orchestrator",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 빌더 산출물 마지막 줄에서 `BUILT: <folder>`를 뽑는다. */
|
|
75
|
+
function parseBuiltFolder(finalText) {
|
|
76
|
+
const lines = String(finalText || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
77
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
78
|
+
const m = lines[i].match(/^BUILT:\s*(.+)$/);
|
|
79
|
+
if (m) return m[1].trim();
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { ensureBuilderAgent, parseBuiltFolder, BUILDER_SLUG, BUILDER_ID, BUILDER_SYSTEM_PROMPT };
|
package/engine/agents/router.cjs
CHANGED
|
@@ -49,8 +49,14 @@ function ensureJudgeRunner(db, runtime) {
|
|
|
49
49
|
model: resolved.model || undefined,
|
|
50
50
|
signal,
|
|
51
51
|
});
|
|
52
|
-
} catch {
|
|
53
|
-
|
|
52
|
+
} catch (error) {
|
|
53
|
+
/*
|
|
54
|
+
* ★사유를 ""로 지우지 않는다 — 여기서 지우면 판정 서비스는 "no connected model
|
|
55
|
+
* reached a valid judgment"라는 거짓 문장만 남긴다(모델은 닿았고, 한도라고
|
|
56
|
+
* 말했다). 러너 계약은 문자열이므로 예외를 그대로 올려 judgeLabels가 사유를
|
|
57
|
+
* 싣게 한다.
|
|
58
|
+
*/
|
|
59
|
+
throw error;
|
|
54
60
|
}
|
|
55
61
|
});
|
|
56
62
|
return judgment;
|