agentlas 1.0.28 → 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 (70) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +5 -2
  3. package/bin/agentlas.cjs +15 -0
  4. package/engine/agentlas-cloud-runtime.cjs +54 -4
  5. package/engine/agentlas-i18n.cjs +4 -4
  6. package/engine/agentlas-input.cjs +10 -3
  7. package/engine/agentlas-judgment.cjs +0 -0
  8. package/engine/agentlas-native-host.cjs +69 -4
  9. package/engine/agentlas-onboard.cjs +20 -0
  10. package/engine/agentlas-ui.cjs +2 -0
  11. package/engine/agentlas-workforce.cjs +69 -13
  12. package/engine/agentlas.cjs +71 -3
  13. package/engine/agents/builder.cjs +84 -0
  14. package/engine/agents/router.cjs +8 -2
  15. package/engine/automation/launchd.cjs +131 -0
  16. package/engine/bootstrap-schema.sql +6 -44
  17. package/engine/browser/cdp.cjs +188 -0
  18. package/engine/browser/vault.cjs +124 -0
  19. package/engine/cli-output.cjs +262 -0
  20. package/engine/cloud-assets/package.cjs +5 -1
  21. package/engine/commands/automation.cjs +37 -1
  22. package/engine/commands/billing.cjs +3 -0
  23. package/engine/commands/browser.cjs +166 -8
  24. package/engine/commands/build.cjs +101 -20
  25. package/engine/commands/connect.cjs +162 -9
  26. package/engine/commands/creds.cjs +65 -3
  27. package/engine/commands/doctor.cjs +66 -4
  28. package/engine/commands/document.cjs +79 -0
  29. package/engine/commands/graph.cjs +1190 -0
  30. package/engine/commands/help.cjs +87 -9
  31. package/engine/commands/hep-cloud.cjs +9 -23
  32. package/engine/commands/hep-hub.cjs +9 -22
  33. package/engine/commands/hep-local.cjs +9 -24
  34. package/engine/commands/hep-network.cjs +9 -35
  35. package/engine/commands/index.cjs +50 -25
  36. package/engine/commands/list.cjs +10 -1
  37. package/engine/commands/mcp.cjs +6 -2
  38. package/engine/commands/native.cjs +18 -2
  39. package/engine/commands/plugin.cjs +22 -0
  40. package/engine/commands/project.cjs +79 -16
  41. package/engine/commands/roles.cjs +210 -0
  42. package/engine/commands/telegram.cjs +23 -16
  43. package/engine/commands/workforce.cjs +63 -12
  44. package/engine/core/desktop-core-fetch.cjs +98 -0
  45. package/engine/core/desktop-core.cjs +170 -0
  46. package/engine/graph/ask-model.cjs +159 -0
  47. package/engine/graph/interview.cjs +960 -0
  48. package/engine/graph/layout.cjs +139 -0
  49. package/engine/graph/package.cjs +223 -0
  50. package/engine/graph/vocabulary.generated.cjs +30 -0
  51. package/engine/hephaestus/local-core.cjs +159 -0
  52. package/engine/hephaestus/runtime.cjs +14 -10
  53. package/engine/project/controller.cjs +8 -8
  54. package/engine/project/team.cjs +99 -0
  55. package/engine/runtime-refusal.cjs +71 -0
  56. package/engine/runtimes/auth-evidence.cjs +78 -0
  57. package/engine/runtimes/detect.cjs +3 -0
  58. package/engine/runtimes/resolve.cjs +1 -1
  59. package/engine/sessions/prompt.cjs +16 -0
  60. package/engine/sessions/session.cjs +24 -2
  61. package/engine/telegram/connect.cjs +202 -0
  62. package/engine/tools/access-notice.cjs +86 -0
  63. package/engine/ui/palette.cjs +7 -3
  64. package/engine/ui/repl.cjs +120 -3
  65. package/engine/vendor/desktop-core.manifest.json +7 -0
  66. package/engine/workforce/capture.cjs +51 -1
  67. package/engine/workforce/deps.cjs +13 -0
  68. package/engine/workforce/local-core-transport.cjs +298 -0
  69. package/package.json +4 -2
  70. package/engine/commands/legacy-network.cjs +0 -29
@@ -1097,10 +1097,15 @@ function validateWorkOrder(value) {
1097
1097
  function validateCandidateSet(value, workOrder, now = new Date(), options = {}) {
1098
1098
  const set = assertObject(value, "candidateSet");
1099
1099
  assertNoForbiddenFitSignals(set);
1100
- assertExactKeys(set, [
1100
+ // projection은 로컬 Core(연합) 응답에만 있는 메뉴 투영 메타데이터다(실측
1101
+ // 2026-08-05, reference-first: fullDossier=false). 원격 서버는 보내지 않는다.
1102
+ // 없애고 되돌려 보내면 Core 쪽 다이제스트 대조가 위험하므로 선택 키로 허용한다.
1103
+ const exactKeys = [
1101
1104
  "schemaVersion", "selectionSessionId", "workOrderId", "ontologyVersion",
1102
1105
  "candidateSetDigest", "decisionOwner", "historyInfluence", "slots", "issuedAt", "expiresAt",
1103
- ], "candidateSet", "candidate_set_invalid");
1106
+ ];
1107
+ if (Object.prototype.hasOwnProperty.call(set, "projection")) exactKeys.push("projection");
1108
+ assertExactKeys(set, exactKeys, "candidateSet", "candidate_set_invalid");
1104
1109
  if (set.schemaVersion !== "agentlas.workforce-candidate-set.v1") fail("candidate_set_invalid", "unsupported candidate set schema");
1105
1110
  assertId(set.selectionSessionId, "candidateSet.selectionSessionId");
1106
1111
  if (set.workOrderId !== workOrder.workOrderId) fail("candidate_set_invalid", "candidate set workOrderId mismatch");
@@ -1126,11 +1131,15 @@ function validateCandidateSet(value, workOrder, now = new Date(), options = {})
1126
1131
  const releases = new Set();
1127
1132
  for (const candidate of assertArray(slotResult.candidates, `candidateSet.${slotId}.candidates`, 100)) {
1128
1133
  assertObject(candidate, "candidate");
1129
- assertExactKeys(candidate, [
1134
+ // missingMandatory는 로컬 Core(연합) 응답에만 있는 미충족 필수 표식이다
1135
+ // (실측 2026-08-05, fullDossier=true에도 동봉). 원격 서버는 보내지 않는다.
1136
+ const candidateKeys = [
1130
1137
  "agentDefinitionId", "agentReleaseId", "releaseVersion", "packageHash", "contentDigest",
1131
1138
  "entityKind", "name", "communities", "fitEvidence", "qualificationEvidence", "optionalGaps",
1132
1139
  "semanticSnapshot", "operational",
1133
- ], "candidate", "candidate_set_invalid");
1140
+ ];
1141
+ if (Object.prototype.hasOwnProperty.call(candidate, "missingMandatory")) candidateKeys.push("missingMandatory");
1142
+ assertExactKeys(candidate, candidateKeys, "candidate", "candidate_set_invalid");
1134
1143
  assertId(candidate.agentDefinitionId, "candidate.agentDefinitionId");
1135
1144
  const releaseId = assertId(candidate.agentReleaseId, "candidate.agentReleaseId");
1136
1145
  if (releases.has(releaseId)) fail("candidate_set_invalid", `duplicate release ${releaseId} in ${slotId}`);
@@ -1151,10 +1160,19 @@ function validateCandidateSet(value, workOrder, now = new Date(), options = {})
1151
1160
  if (typeof operational.callable !== "boolean" || typeof operational.installable !== "boolean") fail("candidate_set_invalid", "candidate operational flags are invalid");
1152
1161
  assertIds(operational.unavailableReasons || [], "candidate.operational.unavailableReasons");
1153
1162
  const semantic = assertObject(candidate.semanticSnapshot, "candidate.semanticSnapshot");
1154
- assertExactKeys(semantic, [
1163
+ // knowledge·modalities는 로컬 Core 스냅샷에만 있는 확장 어휘다(실측 2026-08-05).
1164
+ // 원격 서버는 보내지 않는다 — missingMandatory·projection과 같은 규칙으로
1165
+ // "있으면 검증하고 허용", 원격 계약의 exact-keys는 그대로 둔다.
1166
+ const semanticKeys = [
1155
1167
  "summaries", "roles", "skills", "toolCapabilities", "consumes", "produces",
1156
1168
  "authorities", "runtimes", "languages",
1157
- ], "candidate.semanticSnapshot", "candidate_set_invalid");
1169
+ ];
1170
+ for (const optional of ["knowledge", "modalities"]) {
1171
+ if (Object.prototype.hasOwnProperty.call(semantic, optional)) semanticKeys.push(optional);
1172
+ }
1173
+ assertExactKeys(semantic, semanticKeys, "candidate.semanticSnapshot", "candidate_set_invalid");
1174
+ if (semantic.knowledge !== undefined) assertIds(semantic.knowledge, "candidate.semanticSnapshot.knowledge");
1175
+ if (semantic.modalities !== undefined) assertStrings(semantic.modalities, "candidate.semanticSnapshot.modalities");
1158
1176
  assertStrings(semantic.summaries, "candidate.semanticSnapshot.summaries");
1159
1177
  assertIds(semantic.roles, "candidate.semanticSnapshot.roles");
1160
1178
  assertLeveledConcepts(semantic.skills, "candidate.semanticSnapshot.skills");
@@ -2102,6 +2120,28 @@ function create(deps = {}) {
2102
2120
  tokenLedger.push({ role: role || "stage", runtime: runtimeKind || "?", model: modelPin || null, input, output, cached });
2103
2121
  }
2104
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
+
2105
2145
  async function runModel(runtime, system, prompt, context) {
2106
2146
  const invocation = stageInvocation(runtime, context);
2107
2147
  const executionRuntime = invocation.executionRuntime;
@@ -2116,7 +2156,7 @@ function create(deps = {}) {
2116
2156
  ? `${system}\n\n${localContextSlice}`
2117
2157
  : system;
2118
2158
  if (typeof D.runModel === "function") {
2119
- return normalizeModelResult(await D.runModel({
2159
+ return withTransientModelRetry(context.authorityMode || "no-authority", async () => normalizeModelResult(await D.runModel({
2120
2160
  runtime: executionRuntime,
2121
2161
  system: effectiveSystem,
2122
2162
  prompt,
@@ -2127,7 +2167,7 @@ function create(deps = {}) {
2127
2167
  modelPin: invocation.modelPin,
2128
2168
  effortPin: invocation.effort,
2129
2169
  },
2130
- }));
2170
+ })));
2131
2171
  }
2132
2172
  if (executionRuntime.mode === "cli") {
2133
2173
  const authorityMode = context.authorityMode || "no-authority";
@@ -2156,7 +2196,7 @@ function create(deps = {}) {
2156
2196
  }
2157
2197
  noteIsolationWeakness(executionRuntime.kind, invocation.role);
2158
2198
  }
2159
- const captured = normalizeModelResult(await D.captureRuntime(executionRuntime.kind, effectiveSystem, prompt, {
2199
+ const captureOnce = async () => normalizeModelResult(await D.captureRuntime(executionRuntime.kind, effectiveSystem, prompt, {
2160
2200
  cwd: context.cwd,
2161
2201
  env: context.env,
2162
2202
  permission: context.permission,
@@ -2169,16 +2209,17 @@ function create(deps = {}) {
2169
2209
  outputLimitBytes: authorityMode === "read-only" ? 24 * 1024 * 1024 : undefined,
2170
2210
  envelope: true,
2171
2211
  }));
2212
+ const captured = await withTransientModelRetry(authorityMode, captureOnce);
2172
2213
  recordStageTokens(invocation.role, executionRuntime.kind, invocation.modelPin, captured.usage);
2173
2214
  return captured;
2174
2215
  }
2175
- const viaApi = normalizeModelResult(await D.runApi(
2216
+ const viaApi = await withTransientModelRetry(context.authorityMode || "no-authority", async () => normalizeModelResult(await D.runApi(
2176
2217
  executionRuntime.backend,
2177
2218
  invocation.modelPin,
2178
2219
  effectiveSystem,
2179
2220
  prompt,
2180
2221
  { effort: invocation.effort, envelope: true },
2181
- ));
2222
+ )));
2182
2223
  recordStageTokens(invocation.role, executionRuntime.backend, invocation.modelPin, viaApi.usage);
2183
2224
  return viaApi;
2184
2225
  }
@@ -2393,6 +2434,16 @@ function create(deps = {}) {
2393
2434
 
2394
2435
  async function workforceRun(db, rawTask, ctx = {}) {
2395
2436
  const task = assertString(rawTask, "task", 20_000);
2437
+ // 이 표면이 보는 후보 메뉴의 소스 스코프. 원격 MCP 경로의 정직한 기본은 "hub"
2438
+ // (터미널 workforce = 공개 Hub 메뉴 스태핑). 다른 값은 로컬 Core 전송을 가진
2439
+ // 호출자만 넘길 수 있다 — 여기서 조용히 넓히지 않는다.
2440
+ const sourceScope = (() => {
2441
+ const value = ctx.sourceScope === undefined ? "hub" : ctx.sourceScope;
2442
+ if (!["network", "local", "cloud", "hub"].includes(value)) {
2443
+ fail("source_scope_invalid", `sourceScope must be network|local|cloud|hub, got: ${String(value)}`);
2444
+ }
2445
+ return value;
2446
+ })();
2396
2447
  const ui = ctx.ui || newUi();
2397
2448
  const runtime = ctx.runtime || D.resolveRuntime(db, ctx.runtimeOverride);
2398
2449
  const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : process.cwd());
@@ -2744,7 +2795,9 @@ function create(deps = {}) {
2744
2795
  };
2745
2796
 
2746
2797
  const supersedeCandidateSearch = (workOrder, refinementNumber, triggerKind) => {
2747
- const requestDigest = sha256({ workOrder });
2798
+ // hubStage가 저장한 requestDigest 같은 인자 모양이어야 행을 찾는다 —
2799
+ // search 인자에 sourceScope가 실리므로(2026-08-05) 여기서도 함께 계산한다.
2800
+ const requestDigest = sha256({ workOrder, sourceScope });
2748
2801
  for (const row of receipt.hubTools) {
2749
2802
  if (row.tool !== "workforce.search_candidates" || row.requestDigest !== requestDigest || row.authoritativeChain !== true) continue;
2750
2803
  row.authoritativeChain = false;
@@ -3065,7 +3118,10 @@ function create(deps = {}) {
3065
3118
 
3066
3119
  let refinementsUsed = 0;
3067
3120
  const searchCurrentWorkOrder = async () => {
3068
- const candidateRaw = await hubStage("workforce.search_candidates", { workOrder });
3121
+ // sourceScope는 MCP 스키마상 required다. 예전에는 싣지 않아 서버 기본값
3122
+ // ("hub")에 의존했다 — 기본값이 바뀌면 이 표면의 실제 스코프가 조용히
3123
+ // 넓어지거나 좁아진다. 이 표면이 보는 메뉴를 스스로 선언한다.
3124
+ const candidateRaw = await hubStage("workforce.search_candidates", { workOrder, sourceScope });
3069
3125
  candidateSet = validateCandidateSet(
3070
3126
  candidateRaw,
3071
3127
  workOrder,
@@ -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();
@@ -107,7 +164,13 @@ function main() {
107
164
  if (helpRequested && helpCommand) {
108
165
  const ctx = buildCtx();
109
166
  const command = commands.resolveCommandName(helpCommand);
110
- const code = require("./commands/help.cjs").runForCommand(ctx, command);
167
+ // ★명령이 자기 도움말을 갖고 있으면 그것을 보여준다.
168
+ // 예전에는 무조건 표 한 줄을 긁어(`runForCommand`) "Usage: agentlas graph [options]"
169
+ // 두 줄만 나왔다 — `graph help`에는 8줄짜리 제대로 된 안내가 있는데도 `--help`로는
170
+ // 영원히 닿지 못했다(사용자가 가장 먼저 치는 것이 `--help`다).
171
+ const code = commands.SELF_HELP_COMMANDS.has(command)
172
+ ? commands.dispatch(ctx, [command, "help"])
173
+ : require("./commands/help.cjs").runForCommand(ctx, command);
111
174
  process.exit(typeof code === "number" ? code : 0);
112
175
  }
113
176
  // 옵션 정규화: -h/--help/-V/--version 은 하위 명령으로 변환
@@ -117,12 +180,17 @@ function main() {
117
180
  return a;
118
181
  });
119
182
 
183
+ // 전역 출력 플래그는 명령에 닿기 전에 한 곳에서 뜯어낸다 —
184
+ // 명령마다 --json 유무가 갈리던 것을 구조로 막는다.
185
+ const { options: outputOptions, rest: commandArgv } = parseOutputFlags(normalized);
120
186
  const ctx = buildCtx();
187
+ ctx.output = outputOptions;
121
188
  let code;
122
189
  try {
123
- code = commands.dispatch(ctx, normalized);
190
+ code = commands.dispatch(ctx, commandArgv);
124
191
  } catch (e) {
125
- ctx.err(String((e && e.message) || e));
192
+ // 에러도 같은 형식 규율을 따른다: --json 이면 {"error":{code,message}}.
193
+ ctx.fail(e);
126
194
  process.exit(1);
127
195
  }
128
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;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /*
3
+ * automation/launchd — 앱/창이 꺼져 있어도 자동화를 돌리는 macOS 영속성 (2026-08-06).
4
+ *
5
+ * 배경(오너: "터미널인데 모든 기능이 다 돼야"): 터미널 automation daemon 은 포그라운드
6
+ * setInterval 이라 셸 창을 닫으면 멈춘다 — "데스크탑 없이 자동화가 발동"이 실제론 안 됐다.
7
+ * 데스크탑 electron/launchd/agent.ts 와 같은 방식으로 ~/Library/LaunchAgents 에 plist 를 써서
8
+ * launchctl 로 로드한다. plist 는 coarse StartInterval(기본 300s)마다 `agentlas automation tick`
9
+ * (1회 due 스윕 후 종료)을 poke 한다. DB 가 스케줄 권위이고 plist 는 poke 만 하므로 자동화별
10
+ * plist 동기화가 필요 없다 — 데스크탑과 정확히 같은 계약.
11
+ *
12
+ * ★Label 은 데스크탑("ai.agentlas.automations")과 다르게 둔다("ai.agentlas.cli.automations").
13
+ * 둘 다 설치돼 있어도 공유 DB 의 lease(claimDue)가 이중 실행을 막으므로 공존은 안전하고,
14
+ * 서로의 plist 를 install/uninstall 로 덮지 않게 하려는 것.
15
+ *
16
+ * macOS 전용(launchd). 다른 OS 는 supported:false 로 정직하게 알린다(자동화는 포그라운드
17
+ * `automation daemon` 으로만 — 조용히 안 되는 척하지 않는다).
18
+ */
19
+ const { spawnSync } = require("node:child_process");
20
+ const fs = require("node:fs");
21
+ const os = require("node:os");
22
+ const path = require("node:path");
23
+ const { userDataDir } = require("../core/paths.cjs");
24
+
25
+ const LABEL = "ai.agentlas.cli.automations";
26
+
27
+ function isSupported() { return process.platform === "darwin"; }
28
+ function plistPath() { return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`); }
29
+ function domainTarget() { return `gui/${os.userInfo().uid}`; }
30
+
31
+ /** launchd 가 poke 할 CLI 진입점(절대경로). 전역 설치본이든 체크아웃이든 이 파일 기준으로 해석. */
32
+ function cliEntry() { return path.resolve(__dirname, "..", "..", "bin", "agentlas.cjs"); }
33
+
34
+ function logPath() {
35
+ const dir = path.join(userDataDir(), "logs");
36
+ try { fs.mkdirSync(dir, { recursive: true }); } catch { /* best-effort */ }
37
+ return path.join(dir, "launchd-automations.log");
38
+ }
39
+
40
+ function plistXml(intervalSec = 300) {
41
+ const node = process.execPath;
42
+ const entry = cliEntry();
43
+ const log = logPath();
44
+ const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
45
+ return [
46
+ '<?xml version="1.0" encoding="UTF-8"?>',
47
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
48
+ '<plist version="1.0">',
49
+ "<dict>",
50
+ " <key>Label</key>",
51
+ ` <string>${LABEL}</string>`,
52
+ " <key>ProgramArguments</key>",
53
+ " <array>",
54
+ ` <string>${esc(node)}</string>`,
55
+ ` <string>${esc(entry)}</string>`,
56
+ " <string>automation</string>",
57
+ " <string>tick</string>",
58
+ " </array>",
59
+ " <key>StartInterval</key>",
60
+ ` <integer>${Math.max(30, Math.floor(intervalSec))}</integer>`,
61
+ " <key>RunAtLoad</key>",
62
+ " <true/>",
63
+ " <key>ProcessType</key>",
64
+ " <string>Background</string>",
65
+ " <key>LowPriorityIO</key>",
66
+ " <true/>",
67
+ " <key>StandardOutPath</key>",
68
+ ` <string>${esc(log)}</string>`,
69
+ " <key>StandardErrorPath</key>",
70
+ ` <string>${esc(log)}</string>`,
71
+ "</dict>",
72
+ "</plist>",
73
+ "",
74
+ ].join("\n");
75
+ }
76
+
77
+ /** launchctl 실행 — throw 하지 않고 {code, stderr} 반환(상태 함수가 판정). */
78
+ function launchctl(args) {
79
+ const res = spawnSync("launchctl", args, { encoding: "utf8" });
80
+ return { code: res.status ?? -1, stderr: (res.stderr || "").trim() };
81
+ }
82
+
83
+ function isLoaded() {
84
+ if (!isSupported()) return false;
85
+ return launchctl(["print", `${domainTarget()}/${LABEL}`]).code === 0;
86
+ }
87
+
88
+ function launchdStatus() {
89
+ const supported = isSupported();
90
+ return {
91
+ supported,
92
+ installed: supported && fs.existsSync(plistPath()),
93
+ loaded: supported && isLoaded(),
94
+ plistPath: plistPath(),
95
+ label: LABEL,
96
+ entry: cliEntry(),
97
+ };
98
+ }
99
+
100
+ /** plist 작성 + launchctl bootstrap 로드(멱등 — 이미 로드면 bootout 후 재로드). */
101
+ function enableLaunchd({ intervalSec = 300 } = {}) {
102
+ if (!isSupported()) return { ...launchdStatus(), error: "launchd persistence is macOS-only." };
103
+ const p = plistPath();
104
+ try {
105
+ fs.mkdirSync(path.dirname(p), { recursive: true });
106
+ fs.writeFileSync(p, plistXml(intervalSec), "utf8");
107
+ } catch (err) {
108
+ return { ...launchdStatus(), error: `failed to write plist: ${String(err)}` };
109
+ }
110
+ if (isLoaded()) launchctl(["bootout", `${domainTarget()}/${LABEL}`]);
111
+ const res = launchctl(["bootstrap", domainTarget(), p]);
112
+ if (res.code !== 0 && !isLoaded()) {
113
+ return { ...launchdStatus(), error: res.stderr || "launchctl bootstrap failed." };
114
+ }
115
+ return launchdStatus();
116
+ }
117
+
118
+ /** launchctl bootout + plist 삭제. */
119
+ function disableLaunchd() {
120
+ if (!isSupported()) return launchdStatus();
121
+ if (isLoaded()) launchctl(["bootout", `${domainTarget()}/${LABEL}`]);
122
+ const p = plistPath();
123
+ try { if (fs.existsSync(p)) fs.rmSync(p); }
124
+ catch (err) { return { ...launchdStatus(), error: `failed to remove plist: ${String(err)}` }; }
125
+ return launchdStatus();
126
+ }
127
+
128
+ module.exports = {
129
+ LABEL, plistPath, plistXml, cliEntry, isSupported,
130
+ launchdStatus, enableLaunchd, disableLaunchd,
131
+ };
@@ -412,50 +412,12 @@ CREATE TABLE automation_runs (
412
412
  , last_activity_at TEXT, occurrence_id TEXT, graph_digest TEXT, checkpoint_json TEXT, resume_of_run_id TEXT);
413
413
  CREATE INDEX idx_automation_runs_auto
414
414
  ON automation_runs(automation_id, started_at);
415
- CREATE TRIGGER agentlas_auto_cua_social_insert
416
- AFTER INSERT ON automations
417
- WHEN NEW.tool_mode = 'auto' AND (
418
- lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%reddit%'
419
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%instagram%'
420
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%threads%'
421
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%twitter%'
422
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%x.com%'
423
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%linkedin%'
424
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%facebook%'
425
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%tiktok%'
426
- OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%browser%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%post%')
427
- OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%web%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%login%')
428
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%레딧%'
429
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%인스타%'
430
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%댓글%'
431
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%게시%'
432
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%로그인%'
433
- )
434
- BEGIN
435
- UPDATE automations SET tool_mode = 'computer-use' WHERE id = NEW.id;
436
- END;
437
- CREATE TRIGGER agentlas_auto_cua_social_update
438
- AFTER UPDATE OF name, prompt_template, tool_mode ON automations
439
- WHEN NEW.tool_mode = 'auto' AND (
440
- lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%reddit%'
441
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%instagram%'
442
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%threads%'
443
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%twitter%'
444
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%x.com%'
445
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%linkedin%'
446
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%facebook%'
447
- OR lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%tiktok%'
448
- OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%browser%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%post%')
449
- OR (lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%web%' AND lower(coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'')) LIKE '%login%')
450
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%레딧%'
451
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%인스타%'
452
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%댓글%'
453
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%게시%'
454
- OR coalesce(NEW.name,'') || ' ' || coalesce(NEW.prompt_template,'') LIKE '%로그인%'
455
- )
456
- BEGIN
457
- UPDATE automations SET tool_mode = 'computer-use' WHERE id = NEW.id;
458
- END;
415
+ -- (제거됨 2026-08-06) agentlas_auto_cua_social_insert/update 트리거가 여기 있었다.
416
+ -- 소셜 키워드 목록("twitter/인스타/댓글/게시/로그인"…)으로 tool_mode를 computer-use로
417
+ -- 강제 되돌리던 DB 차원 단어목록 판정 — 코드의 단어목록을 LLM 판정으로 대체할 때
418
+ -- 트리거만 살아남아, 코드 리뷰가 수 없는 곳에서 toolMode 도출 규칙을 무효화했다
419
+ -- (실측: UPDATE tool_mode='auto' 같은 연결에서 즉시 되돌아왔다). 판정을 DB 트리거로
420
+ -- 만들지 않는다 판정은 코드·게이트가 보는 곳에만 산다.
459
421
  CREATE TABLE agent_evolution_proposals (
460
422
  id TEXT PRIMARY KEY,
461
423
  agent_id TEXT NOT NULL,