agentlas 1.0.29 → 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/bin/agentlas.cjs +15 -0
  3. package/engine/agentlas-cloud-runtime.cjs +54 -4
  4. package/engine/agentlas-input.cjs +10 -1
  5. package/engine/agentlas-judgment.cjs +0 -0
  6. package/engine/agentlas-native-host.cjs +69 -4
  7. package/engine/agentlas-ui.cjs +2 -0
  8. package/engine/agentlas-workforce.cjs +28 -5
  9. package/engine/agentlas.cjs +64 -2
  10. package/engine/agents/builder.cjs +84 -0
  11. package/engine/agents/router.cjs +8 -2
  12. package/engine/automation/launchd.cjs +131 -0
  13. package/engine/bootstrap-schema.sql +6 -44
  14. package/engine/browser/cdp.cjs +188 -0
  15. package/engine/browser/vault.cjs +124 -0
  16. package/engine/cli-output.cjs +262 -0
  17. package/engine/cloud-assets/package.cjs +5 -1
  18. package/engine/commands/automation.cjs +37 -1
  19. package/engine/commands/browser.cjs +166 -8
  20. package/engine/commands/build.cjs +101 -20
  21. package/engine/commands/connect.cjs +162 -9
  22. package/engine/commands/creds.cjs +16 -2
  23. package/engine/commands/doctor.cjs +20 -1
  24. package/engine/commands/document.cjs +79 -0
  25. package/engine/commands/graph.cjs +50 -10
  26. package/engine/commands/help.cjs +8 -6
  27. package/engine/commands/index.cjs +1 -0
  28. package/engine/commands/list.cjs +10 -1
  29. package/engine/commands/project.cjs +79 -16
  30. package/engine/commands/roles.cjs +10 -2
  31. package/engine/commands/telegram.cjs +23 -16
  32. package/engine/core/desktop-core-fetch.cjs +98 -0
  33. package/engine/core/desktop-core.cjs +170 -0
  34. package/engine/graph/ask-model.cjs +29 -1
  35. package/engine/graph/interview.cjs +105 -20
  36. package/engine/graph/layout.cjs +36 -34
  37. package/engine/graph/vocabulary.generated.cjs +1 -1
  38. package/engine/hephaestus/runtime.cjs +10 -2
  39. package/engine/project/controller.cjs +8 -8
  40. package/engine/project/team.cjs +99 -0
  41. package/engine/runtime-refusal.cjs +71 -0
  42. package/engine/runtimes/detect.cjs +3 -0
  43. package/engine/runtimes/resolve.cjs +1 -1
  44. package/engine/sessions/session.cjs +15 -2
  45. package/engine/telegram/connect.cjs +202 -0
  46. package/engine/ui/palette.cjs +1 -0
  47. package/engine/ui/repl.cjs +112 -1
  48. package/engine/vendor/desktop-core.manifest.json +7 -0
  49. package/engine/workforce/capture.cjs +51 -1
  50. 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
- for (const file of files) {
44
- if (file.path === "agentlas.json") continue;
45
- h.update(file.path);
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().length ? [...new Set(skills)].sort() : ["agentlas-package"];
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
- state.selectedCommand = list[state.selected].command;
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) st.error = obj.result || "claude 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
- if (obj.status && obj.status !== "success") st.error = `gemini ${obj.status}`;
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
- finish({ text, session: st.session, usage: st.usage, error: exitError });
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();
@@ -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 captured = normalizeModelResult(await D.captureRuntime(executionRuntime.kind, effectiveSystem, prompt, {
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
  }
@@ -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, normalized);
190
+ code = commands.dispatch(ctx, commandArgv);
130
191
  } catch (e) {
131
- ctx.err(String((e && e.message) || e));
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 };
@@ -49,8 +49,14 @@ function ensureJudgeRunner(db, runtime) {
49
49
  model: resolved.model || undefined,
50
50
  signal,
51
51
  });
52
- } catch {
53
- return "";
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;