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
@@ -21,6 +21,7 @@ const SLASH_COMMANDS = [
21
21
  { command: "/rm", args: "<n>", ko: "세션 제거", en: "Remove a session" },
22
22
  { command: "/agents", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
23
23
  { command: "/list", args: "", ko: "설치 에이전트 목록", en: "List installed agents" },
24
+ { command: "/graph", args: "[run <이름>]", ko: "저장된 자동화 그래프", en: "Saved automation graphs" },
24
25
  { command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
25
26
  { command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
26
27
  { command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
@@ -39,9 +40,10 @@ const SLASH_COMMANDS = [
39
40
  { command: "/network", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
40
41
  { command: "/workforce", args: "<request>", ko: "Workforce 라우트", en: "Workforce route" },
41
42
  { command: "/taskforce", args: "<request>", ko: "임시 태스크포스 편성", en: "Assemble a task force" },
42
- // 소스 스코프가 이름에 붙은 스태핑 3종. 스코프를 문장에 적는다 예전에는
43
- // 별칭이 스코프를 버려 /hep-cloud 자산 보관함으로, /hep-hub 검색으로
44
- // 갔다(2026-07-28 수리). 팔레트 문구가 사용자에게 하는 약속이다.
43
+ // 소스 스코프 편성 4종 2026-08-05 같은 삭제 네이티브 배선으로 복원.
44
+ // 이전에는 외부 CLI 스텁(exit 3)이라 죽은 메뉴였다. 지금은 터미널의 편성
45
+ // 루프가 직접 돌고 로컬 Agentlas-OS Core가 선언된 스코프의 메뉴를 연합한다.
46
+ // 팔레트 문구가 곧 사용자에게 하는 약속이다 — 스코프를 문장에 적는다.
45
47
  { command: "/hep-network", args: "\"<request>\"", ko: "로컬+오너 클라우드+공개 Hub 연합 편성", en: "Staff across Local + owner Cloud + public Hub" },
46
48
  { command: "/hep-local", args: "\"<request>\"", ko: "등록된 로컬 에이전트만으로 편성", en: "Staff from registered Local agents only" },
47
49
  { command: "/hep-cloud", args: "\"<request>\"", ko: "오너 Agent Cloud만으로 편성", en: "Staff from owner Agent Cloud only" },
@@ -75,6 +77,8 @@ const SLASH_COMMANDS = [
75
77
  { command: "/creds", args: "<sub>", ko: "자격증명", en: "Credentials" },
76
78
  { command: "/env", args: "", ko: "공유 환경 키", en: "Shared env keys" },
77
79
  { command: "/multimodal", args: "", ko: "이미지·영상·음성 설정", en: "Image/video/audio providers" },
80
+ { command: "/document", args: "pdf <html|url>", ko: "문서 PDF 내보내기", en: "Export a document to PDF" },
81
+ { command: "/roles", args: "[set <role> <runtime>]", ko: "오케스트레이터·워커 모델 역할 조회/설정", en: "Show or set orchestrator/worker model roles" },
78
82
  { command: "/telegram", args: "[sub]", ko: "텔레그램 연결", en: "Telegram bindings" },
79
83
  { command: "/oberon", args: "[sub]", ko: "AI 필름", en: "AI film" },
80
84
  { command: "/film", args: "<sub>", ko: "필름 렌더", en: "Film render" },
@@ -132,6 +132,36 @@ async function startRepl(ctx, opts = {}) {
132
132
  let effortOverride = opts.effort || null;
133
133
 
134
134
  const recoverPresentation = (operation, error) => {
135
+ /*
136
+ * 정직 정지(honestStop)는 복구 대상이 아니다 (2026-08-06 실사용 실측).
137
+ * 실제 REPL에 자연어 지시를 하면 "이 프로젝트에 팀이 없다"는 정직한 안내가
138
+ * "One이 복구 중" 한 줄로 삼켜졌다 — 사용자는 무엇이 왜 안 되는지, 무엇을
139
+ * 하면 되는지 전혀 못 봤다(빈 답보다 나쁜 침묵). 정직 정지는 그 사유를
140
+ * 그대로 보여주고, 어떻게 실행하는지까지 덧붙인다. One 복구는 예상 못 한
141
+ * 실패에만 쓴다. (사람용 문장과 기계 판단은 다른 필드 — 스케줄러와 같은 원칙.)
142
+ */
143
+ if (error && (error.honestStop || error.code)) {
144
+ // 공유 controller 모듈은 lang을 모른다 — 기계 code로 여기서 현지화한다.
145
+ const KO_REASON = {
146
+ project_team_empty: "이 프로젝트에 에이전트 팀이 없습니다. Agentlas Desktop의 Work에서 에이전트를 넣고 다시 시도하세요.",
147
+ project_not_connected: "이 폴더는 Agentlas 프로젝트에 연결돼 있지 않습니다. Desktop Work에서 연결하거나, 특정 에이전트를 지정해 바로 실행하세요.",
148
+ project_ambiguous: "이 폴더에 Agentlas 프로젝트가 둘 이상 연결돼 있습니다. 소스 연결을 하나만 남기고 다시 시도하세요.",
149
+ project_team_unreadable: "이 프로젝트의 에이전트 팀을 읽을 수 없습니다. Agentlas Desktop에서 프로젝트를 열어 팀을 다시 저장하세요.",
150
+ project_teams_unsupported: "이 Agentlas 데이터 저장소는 아직 프로젝트 팀을 지원하지 않습니다. 최신 Agentlas Desktop을 한 번 실행한 뒤 다시 시도하세요.",
151
+ };
152
+ const reason = (!en && error.code && KO_REASON[error.code]) || String(error.message || error);
153
+ ui.line(ui.c.amber("✖ ") + reason);
154
+ // 데스크탑을 강제하지 않는다 — 터미널에서 바로 프로젝트를 연결한다(오너 원칙).
155
+ const connectHint = (error.code === "project_not_connected" || error.code === "project_team_empty");
156
+ ui.line(ui.c.dim(en
157
+ ? (connectHint
158
+ ? "Connect this folder standalone: agentlas project use <agent> · list agents: /list · or run one directly: agentlas run <agent> \"<task>\"."
159
+ : "Run a specific agent directly: agentlas run <agent> \"<task>\" · list agents: /list.")
160
+ : (connectHint
161
+ ? "이 폴더를 바로 연결: agentlas project use <에이전트> · 에이전트 목록: /list · 또는 하나만 바로 실행: agentlas run <에이전트> \"<할 일>\"."
162
+ : "특정 에이전트로 바로 실행: agentlas run <에이전트> \"<할 일>\" · 에이전트 목록: /list.")));
163
+ return;
164
+ }
135
165
  const active = orch.active();
136
166
  const evidence = String((error && error.message) || error || "").slice(0, 8000);
137
167
  if (active && !active.isBusy()) {
@@ -252,10 +282,37 @@ async function startRepl(ctx, opts = {}) {
252
282
  },
253
283
  });
254
284
  const PROMPT = "› ";
285
+ /*
286
+ * 빈 입력 고스트 힌트 (2026-08-06, 레퍼런스 REPL 대조에서 나온 유도 격차).
287
+ * claude-code류 REPL은 빈 입력줄에 무엇을 칠 수 있는지 흐리게 보여준다 —
288
+ * 우리는 `› ` 뿐이라 첫 사용자가 / 팔레트·@ 멘션·? 도움의 존재를 알 길이
289
+ * 없었다. 커서 "뒤"에 dim 힌트를 쓰고 커서를 되돌린다. 어떤 키든 처음
290
+ * 눌리는 순간 커서-뒤 지우기(CSI K)로 걷어내므로 readline 의 echo·팔레트
291
+ * 오버레이와 겹치지 않는다. 비TTY·바쁜 세션에서는 그리지 않는다.
292
+ */
293
+ const { visWidth } = require("../agentlas-composer.cjs");
294
+ const GHOST_HINT = en
295
+ ? "type a task · / commands · @ files · ? shortcuts"
296
+ : "할 일을 문장으로 · / 명령 · @ 파일 · ? 단축키";
297
+ let ghostVisible = false;
298
+ const drawGhost = () => {
299
+ if (!process.stdout.isTTY || rl.line) return;
300
+ const width = visWidth(GHOST_HINT);
301
+ const room = (process.stdout.columns || 80) - visWidth(PROMPT) - 2;
302
+ if (width > room) return; // 좁은 터미널에서는 유도보다 입력이 우선이다
303
+ ui.write(ui.c.faint(GHOST_HINT) + `\x1b[${width}D`);
304
+ ghostVisible = true;
305
+ };
306
+ const clearGhost = () => {
307
+ if (!ghostVisible) return;
308
+ ghostVisible = false;
309
+ if (process.stdout.isTTY) ui.write("\x1b[K");
310
+ };
255
311
  const prompt = () => {
256
312
  if (!renderer.session || !renderer.session.isBusy()) {
257
313
  rl.setPrompt(PROMPT);
258
314
  rl.prompt();
315
+ drawGhost();
259
316
  }
260
317
  };
261
318
 
@@ -289,6 +346,8 @@ async function startRepl(ctx, opts = {}) {
289
346
  },
290
347
  });
291
348
  const onShortcutKey = (str, key) => {
349
+ // 고스트 힌트는 어떤 키가 와도 readline 이 echo 하기 전에 걷어낸다.
350
+ clearGhost();
292
351
  if (!permissionShortcut.handleKey(str, key)) return;
293
352
  swallowCompletion = true;
294
353
  setImmediate(() => { swallowCompletion = false; });
@@ -329,10 +388,62 @@ async function startRepl(ctx, opts = {}) {
329
388
  }
330
389
  };
331
390
 
391
+ let emptyEnters = 0;
332
392
  rl.on("line", (line) => {
333
393
  sigints = 0;
334
394
  const input = line.trim();
335
- if (!input) { prompt(); return; }
395
+ if (!input) {
396
+ /*
397
+ * 빈 Enter 유도 (2026-08-06 레퍼런스 대조): 1회는 조용히 — 실수로 친
398
+ * Enter 마다 잔소리하면 소음이다. 연달아 두 번이면 길을 잃은 것이므로
399
+ * 다음 행선지를 한 줄로 보여준다.
400
+ */
401
+ emptyEnters += 1;
402
+ if (emptyEnters >= 2) {
403
+ emptyEnters = 0;
404
+ ui.line(ui.c.dim(en
405
+ ? "Type a task in plain words, / for commands, ? for shortcuts."
406
+ : "할 일을 그냥 문장으로 치세요. / 는 명령, ? 는 단축키입니다."));
407
+ }
408
+ prompt();
409
+ return;
410
+ }
411
+ emptyEnters = 0;
412
+
413
+ /*
414
+ * `?` 단축키 도움 (2026-08-06): 첫 화면 어디에도 키 조작법이 없었다 —
415
+ * Shift-Tab 권한 순환·조종 큐·@멘션은 아는 사람만 썼다. 한 글자로 요약을
416
+ * 준다(claude-code 의 ? 관례).
417
+ */
418
+ if (input === "?") {
419
+ const rowsHelp = en ? [
420
+ ["/", "command palette (type to filter, ↑↓ pick, Tab complete, Esc close)"],
421
+ ["Tab", "complete commands, agent names, @file paths, session keys"],
422
+ ["@path", "mention a file anywhere in your sentence"],
423
+ ["↑ / ↓", "input history (kept across sessions)"],
424
+ ["Shift-Tab", "cycle permission read → write (twice for full)"],
425
+ ["Ctrl-C", "interrupt the running turn · press twice to quit"],
426
+ ["typing while running", "queues steering for the current turn"],
427
+ ["/sessions · /s <n>", "list · switch parallel sessions"],
428
+ ] : [
429
+ ["/", "명령 팔레트 (입력=검색, ↑↓ 선택, Tab 완성, Esc 닫기)"],
430
+ ["Tab", "명령·에이전트 이름·@파일 경로·세션 키 완성"],
431
+ ["@경로", "문장 어디서든 파일 멘션"],
432
+ ["↑ / ↓", "입력 히스토리 (세션 넘어 유지)"],
433
+ ["Shift-Tab", "권한 순환 read → write (두 번이면 full)"],
434
+ ["Ctrl-C", "실행 중 턴 중단 · 두 번이면 종료"],
435
+ ["실행 중 타이핑", "현재 턴에 조종 지시로 쌓임"],
436
+ ["/sessions · /s <n>", "병렬 세션 목록 · 전환"],
437
+ ];
438
+ // 한글 키 라벨은 전각 폭 — padEnd(코드포인트 수)로 맞추면 열이 어긋난다.
439
+ const keyWidth = Math.max(...rowsHelp.map(([k]) => visWidth(k)));
440
+ ui.line(ui.c.bold(en ? "Shortcuts" : "단축키"));
441
+ for (const [k, desc] of rowsHelp) {
442
+ ui.line(" " + ui.c.blue(k) + " ".repeat(keyWidth - visWidth(k)) + " " + ui.c.dim(desc));
443
+ }
444
+ prompt();
445
+ return;
446
+ }
336
447
 
337
448
  if (input.startsWith("/")) {
338
449
  try {
@@ -585,8 +696,12 @@ function handleSlash(ctx, cmdline, api) {
585
696
 
586
697
  case "runtime": {
587
698
  if (!rest[0]) throw new Error("Usage: /runtime claude-code|codex|gemini");
699
+ // 세션 오버라이드는 저장되지 않는다 — 고지 없이는 사용자가 영구 설정으로
700
+ // 믿는다(2026-08-05 감사 결함 C). 영구 경로를 같은 줄에서 알려준다.
588
701
  api.setRuntime(rest[0]);
589
- ctx.out(ui.c.dim(`runtime: ${rest[0]} (${en ? "applies to new sessions" : "새 세션부터 적용"})`));
702
+ ctx.out(ui.c.dim(`runtime: ${rest[0]} (${en
703
+ ? "new sessions in this REPL only — persist with: agentlas roles set orchestrator " + rest[0]
704
+ : "이 REPL의 새 세션 한정 — 영구 설정: agentlas roles set orchestrator " + rest[0]})`));
590
705
  return;
591
706
  }
592
707
  case "model": {
@@ -595,7 +710,9 @@ function handleSlash(ctx, cmdline, api) {
595
710
  const next = ["default", "inherit"].includes(model.toLowerCase()) ? null : model;
596
711
  api.setModel(next);
597
712
  ctx.out(ui.c.dim(
598
- `model: ${next || "default"} (${en ? "applies to new sessions" : "새 세션부터 적용"})`,
713
+ `model: ${next || "default"} (${en
714
+ ? "new sessions in this REPL only — persist with: agentlas roles set"
715
+ : "이 REPL의 새 세션 한정 — 영구 설정: agentlas roles set"})`,
599
716
  ));
600
717
  return;
601
718
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": "2",
3
+ "url": "https://github.com/agentlas-ai/agentlas-terminal/releases/download/desktop-core-v2/desktop-core.tar.gz",
4
+ "sha256": "782760c34af1d06b9efd212d48ba032fe46907bdc8bc836025c44fc658968d6f",
5
+ "sizeBytes": 12295321,
6
+ "writtenAt": "2026-08-09T21:43:34.707Z"
7
+ }
@@ -18,6 +18,7 @@
18
18
  * 프로젝트 .env가 보호 키(타임아웃/캡처 상한 포함)를 덮지 못한다
19
19
  */
20
20
  const fs = require("node:fs");
21
+ const { detectRuntimeRefusal } = require("../runtime-refusal.cjs");
21
22
  const os = require("node:os");
22
23
  const path = require("node:path");
23
24
  const { spawn } = require("node:child_process");
@@ -309,6 +310,48 @@ function capturedRuntimeUsage(kind, raw) {
309
310
  return null;
310
311
  }
311
312
 
313
+
314
+ /**
315
+ * ★캡처된 스트림에서 **실패 표식**을 걷는다 — 종료코드만 보면 exit 0 거절이 산출물이 된다.
316
+ *
317
+ * 실측(2026-08-06): claude 한도는 rate_limit_event(status:rejected)·result(is_error:true)를
318
+ * 보내고, codex 한도는 표식 없이 거절문을 agent_message로 싣고 turn.completed로 끝난다.
319
+ * 이 파일은 이벤트 종류 목록에 rate_limit_event를 적어 두고도 한 번도 읽지 않았다 —
320
+ * 거절문이 그대로 워커 핸드오프가 되어 종합(synthesis)에 섞였다.
321
+ */
322
+ function capturedRuntimeFailure(kind, raw, text) {
323
+ const events = raw.split(/\r?\n/).map((line) => {
324
+ try { return JSON.parse(line); } catch { return null; }
325
+ }).filter(Boolean);
326
+ for (const event of events) {
327
+ if (!event || typeof event !== "object") continue;
328
+ if (kind === "claude-code") {
329
+ if (event.type === "result" && event.is_error) {
330
+ return { message: typeof event.result === "string" ? event.result : "claude error", source: "marker" };
331
+ }
332
+ if (event.type === "rate_limit_event" && event.rate_limit_info && event.rate_limit_info.status === "rejected") {
333
+ return { message: "claude rate limit rejected", source: "marker" };
334
+ }
335
+ }
336
+ if (kind === "codex" && (event.type === "turn.failed" || event.type === "error")) {
337
+ const msg = (event.error && (event.error.message || event.error)) || "codex error";
338
+ return { message: String(msg), source: "marker" };
339
+ }
340
+ if (kind === "gemini") {
341
+ if (event.type === "error") {
342
+ const msg = (event.error && (event.error.message || event.error)) || "gemini error";
343
+ return { message: String(msg), source: "marker" };
344
+ }
345
+ if (event.type === "result" && event.status && event.status !== "success") {
346
+ return { message: `gemini ${event.status}`, source: "marker" };
347
+ }
348
+ }
349
+ }
350
+ // 표식이 전혀 없는 케이스(codex 한도) — 휴리스틱 최후 그물, 출처 표기.
351
+ const refusal = detectRuntimeRefusal(text);
352
+ return refusal ? { message: refusal.message, source: "heuristic" } : null;
353
+ }
354
+
312
355
  function capturedRuntimeAgentText(kind, raw) {
313
356
  const text = String(raw || "");
314
357
  const events = [];
@@ -377,7 +420,8 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
377
420
  ? captureOutputLimit(opts.env || process.env)
378
421
  : directCaptureOutputLimit(opts.outputLimitBytes);
379
422
  return new Promise((resolve, reject) => {
380
- const bin = which(RUNTIME_BIN[kind]) || RUNTIME_BIN[kind];
423
+ // 계약 테스트용 실행 파일 주입(가짜 CLI가 픽스처를 cat) 프로덕션 경로에선 없음.
424
+ const bin = opts.binOverride || which(RUNTIME_BIN[kind]) || RUNTIME_BIN[kind];
381
425
  let child;
382
426
  try {
383
427
  const spawnImpl = opts.spawn || spawn;
@@ -523,6 +567,12 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
523
567
  } else {
524
568
  text = raw;
525
569
  }
570
+ // ★거절 고지문은 산출물이 아니다 — 성공으로 돌려주면 워커 핸드오프가 오염된다.
571
+ const failure = capturedRuntimeFailure(kind, raw, text);
572
+ if (failure) {
573
+ finishReject(new Error(`${kind} runtime refused (${failure.source}): ${failure.message}`));
574
+ return;
575
+ }
526
576
  finishResolve(opts.envelope
527
577
  ? { text, usage: capturedRuntimeUsage(kind, raw) }
528
578
  : text);
@@ -579,9 +579,22 @@ function workforceRuntime(ctx = {}) {
579
579
  return _workforce;
580
580
  }
581
581
 
582
+ /*
583
+ * 로컬 Core 전송을 실은 1회용 런타임 (2026-08-05, hep-* 네이티브 배선).
584
+ * 싱글턴을 쓰지 않는 이유: D.callHubTool은 명령 수명의 stdio 프로세스와 validate
585
+ * 계보 상태를 붙잡는다 — 공유하면 다음 편성이 앞 편성의 계보를 이어받는다.
586
+ * 원격 기본 경로(workforceRuntime)는 여기서 아무것도 바뀌지 않는다.
587
+ */
588
+ function createLocalCoreWorkforceRuntime(ctx, transport) {
589
+ const deps = buildWorkforceDeps(ctx);
590
+ deps.callHubTool = transport.callHubTool;
591
+ return require("../agentlas-workforce.cjs").create(deps);
592
+ }
593
+
582
594
  module.exports = {
583
595
  buildWorkforceDeps,
584
596
  workforceRuntime,
597
+ createLocalCoreWorkforceRuntime,
585
598
  resolveWorkforceRuntime,
586
599
  receiptFile,
587
600
  appendReceipt,
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ /*
3
+ * workforce/local-core-transport — 편성 루프(agentlas-workforce.cjs)를 로컬
4
+ * Agentlas Core(연합 소유자)에 잇는 어댑터.
5
+ *
6
+ * 전선 계약 — 전부 2026-08-05 실측(모델 호출 0회 프로브)으로 확정:
7
+ * search_candidates 요청 {workOrder, sourceScope}
8
+ * 응답 agentlas.workforce-federation-result.v1 봉투
9
+ * → 루프에는 봉투를 벗긴 candidateSet만 준다.
10
+ * validate_selection 요청 {workOrder, selection, candidateSet}
11
+ * (federationResult를 실으면 invalid_federation_result —
12
+ * Core는 자기 선택 세션에서 연합을 이미 안다)
13
+ * 응답 안의 selectionValidation이 정확히
14
+ * agentlas.workforce-selection-validation.v1 — 루프 검증기와
15
+ * 동일 계약이라 그대로 돌려준다. 원본 응답은 여기 상태로
16
+ * 붙잡아 둔다(prepare가 요구).
17
+ * prepare_execution 요청 {workOrder, selection, candidateSet,
18
+ * federatedSelection: <validate 원본 응답>,
19
+ * validationReceipt: <동일>, projectDir}
20
+ * 응답 안의 executionPlan이 정확히
21
+ * agentlas.workforce-execution-plan.v5 (roster에
22
+ * directiveBundle·permissionPolicy 동봉) — 그대로 돌려준다.
23
+ *
24
+ * 원격(agentlas.cloud) 경로는 건드리지 않는다: 이 어댑터는 D.callHubTool로
25
+ * 주입될 때만 산다. Core 거절 코드는 원문 그대로 전파된다(local-core.cjs 계약).
26
+ */
27
+ const crypto = require("node:crypto");
28
+ const { createLocalCoreClient } = require("../hephaestus/local-core.cjs");
29
+
30
+ const SOURCE_SCOPES = new Set(["network", "local", "cloud", "hub"]);
31
+
32
+ /*
33
+ * ── id 정규화 (실측 2026-08-05 라이브 런 실패의 수리) ──
34
+ *
35
+ * 리더 LLM은 사람이 읽는 id를 짓는다(work-order:korean-summary-…-20260805,
36
+ * slotId "korean-doc-writer"). 원격 서버는 받아주지만 로컬 Core 경계는 finite id
37
+ * 정책으로 거절한다 — 실측 issues: work_order_id_not_public_finite,
38
+ * slot_id_not_public_finite. novel id 의 유효 형식은 <ns>:opaque-<64hex> 다.
39
+ *
40
+ * 그래서 Core 로 나가는 모든 workOrderId·slotId 를 결정론적 opaque 로 정규화하고
41
+ * (sha256(원본) — 같은 원본은 항상 같은 opaque, 재검색·재개에도 안정), Core 에서
42
+ * 들어오는 응답의 그 값들을 원본으로 역치환한다. 루프·리더 프롬프트·영수증은
43
+ * 사람이 읽는 원본만 본다. 치환은 "문자열 값의 정확 일치"로만 한다 — 부분 문자열
44
+ * 치환은 다이제스트·서술문을 오염시킨다.
45
+ */
46
+ const sha256hex = (value) => crypto.createHash("sha256").update(String(value)).digest("hex");
47
+ const OPAQUE_RE = /^[a-z][a-z-]*:(?:opaque-[0-9a-f]{64}|ordinal-\d+)$/;
48
+
49
+ function opaqueId(namespace, id) {
50
+ return OPAQUE_RE.test(id) ? id : `${namespace}:opaque-${sha256hex(id)}`;
51
+ }
52
+
53
+ /** workOrder에서 forward(원본→opaque)/reverse(opaque→원본) 값 지도를 만든다. */
54
+ function buildIdMaps(workOrder) {
55
+ const forward = new Map();
56
+ const reverse = new Map();
57
+ const add = (namespace, id) => {
58
+ if (typeof id !== "string" || !id) return;
59
+ const mapped = opaqueId(namespace, id);
60
+ if (mapped === id) return;
61
+ forward.set(id, mapped);
62
+ reverse.set(mapped, id);
63
+ };
64
+ add("work-order", workOrder.workOrderId);
65
+ for (const slot of workOrder.roleSlots || []) add("slot", slot.slotId);
66
+ return { forward, reverse };
67
+ }
68
+
69
+ /*
70
+ * 경계 거절 issues 의 path("edges[0].artifactKinds[0]")에서 실제 값을 찾는다.
71
+ * 선제 정규화(workOrderId·slotId)가 못 덮는 finite 어휘가 있다 — 실측:
72
+ * artifactKinds 는 finite 카탈로그다(artifact_concept_not_public_finite). 카탈로그를
73
+ * 하드코딩하면 Core 업데이트마다 어긋나므로, 거절이 지목한 값만 opaque 화해
74
+ * 1회 재시도한다. role/skill/community 는 open-world 라 여기 올 일이 없다.
75
+ */
76
+ function valueAtPath(root, issuePath) {
77
+ const segments = String(issuePath || "").match(/[A-Za-z_][A-Za-z0-9_]*|\[\d+\]/g) || [];
78
+ let current = root;
79
+ for (const segment of segments) {
80
+ if (current == null) return undefined;
81
+ current = segment.startsWith("[") ? current[Number(segment.slice(1, -1))] : current[segment];
82
+ }
83
+ return current;
84
+ }
85
+
86
+ function boundaryIssues(error) {
87
+ const issues = error?.detail?.boundary?.issues;
88
+ return Array.isArray(issues) ? issues : null;
89
+ }
90
+
91
+ /** 깊은 순회로 문자열 값을 정확 일치 치환한 사본을 만든다. */
92
+ function mapDeep(value, map) {
93
+ if (map.size === 0) return value;
94
+ if (typeof value === "string") return map.get(value) || value;
95
+ if (Array.isArray(value)) return value.map((item) => mapDeep(item, map));
96
+ if (value && typeof value === "object") {
97
+ const out = {};
98
+ for (const [key, item] of Object.entries(value)) out[key] = mapDeep(item, map);
99
+ return out;
100
+ }
101
+ return value;
102
+ }
103
+
104
+ function createLocalCoreHubTool({ sourceScope, projectDir, cwd, client } = {}) {
105
+ if (!SOURCE_SCOPES.has(sourceScope)) {
106
+ const error = new Error(`sourceScope must be network|local|cloud|hub, got: ${String(sourceScope)}`);
107
+ error.code = "source_scope_invalid";
108
+ throw error;
109
+ }
110
+ if (!projectDir || typeof projectDir !== "string") {
111
+ const error = new Error("projectDir is required — local Core prepare_execution binds the preparation to a project");
112
+ error.code = "project_dir_required";
113
+ throw error;
114
+ }
115
+ const core = client || createLocalCoreClient({ cwd: cwd || projectDir });
116
+ // 편성 계보 상태 — 전부 "Core 어휘"(opaque id) 원본이다:
117
+ // maps: 마지막 search 의 id 지도. 재검색(refinement)마다 재구성된다.
118
+ // coreCandidateSet: Core 가 준 원본 CandidateSet (validate/prepare 로 무수정 반송).
119
+ // lastValidationEnvelope: validate 원본 응답. prepare 의 federatedSelection 은
120
+ // 이것이어야 한다 — 루프가 들고 있는 것은 벗겨낸 selectionValidation 뿐이다.
121
+ let maps = { forward: new Map(), reverse: new Map() };
122
+ let coreCandidateSet = null;
123
+ let lastValidationEnvelope = null;
124
+ // validate 가 실제로 수락한 Core 어휘 selection. prepare 는 정확히 이 본을
125
+ // 반송해야 한다 — 반응형 reasonCode 수리가 있었으면 루프의 selection 을 다시
126
+ // 변환한 것과 다르다(exact binding).
127
+ let lastCoreSelection = null;
128
+
129
+ const invalid = (message) => {
130
+ const error = new Error(message);
131
+ error.code = "local_core_invalid_response";
132
+ return error;
133
+ };
134
+
135
+ async function callHubTool(name, args) {
136
+ if (name === "workforce.search_candidates") {
137
+ maps = buildIdMaps(args.workOrder);
138
+ coreCandidateSet = null;
139
+ lastValidationEnvelope = null;
140
+ // fullDossier: 터미널 루프의 후보 검증기는 legacy full-echo 계약이다
141
+ // (qualificationEvidence·packageHash·contentDigest 필수 — 실측: 기본
142
+ // reference-first 메뉴는 candidate_set_invalid 로 거절된다).
143
+ let envelope;
144
+ try {
145
+ envelope = await core.call(name, { workOrder: mapDeep(args.workOrder, maps.forward), sourceScope, fullDossier: true });
146
+ } catch (error) {
147
+ // 반응형 정규화(1회): 경계가 지목한 finite 값만 opaque 로 바꿔 재시도.
148
+ const issues = error.code === "work_order_hub_boundary_rejected" ? boundaryIssues(error) : null;
149
+ if (!issues || !issues.length) throw error;
150
+ const sentWorkOrder = mapDeep(args.workOrder, maps.forward);
151
+ let repaired = 0;
152
+ for (const issue of issues) {
153
+ const value = valueAtPath(sentWorkOrder, issue.path);
154
+ if (typeof value !== "string" || !value) continue;
155
+ const namespace = value.includes(":") ? value.slice(0, value.indexOf(":")) : "id";
156
+ const original = maps.reverse.get(value) || value;
157
+ const mapped = opaqueId(namespace, value);
158
+ if (mapped === value) continue;
159
+ maps.forward.set(original, mapped);
160
+ maps.reverse.set(mapped, original);
161
+ repaired += 1;
162
+ }
163
+ if (!repaired) throw error;
164
+ envelope = await core.call(name, { workOrder: mapDeep(args.workOrder, maps.forward), sourceScope, fullDossier: true });
165
+ }
166
+ const candidateSet = envelope && envelope.candidateSet;
167
+ if (!candidateSet || typeof candidateSet !== "object") throw invalid("local Core federation returned no candidateSet");
168
+ coreCandidateSet = candidateSet;
169
+ return mapDeep(candidateSet, maps.reverse);
170
+ }
171
+ if (name === "workforce.validate_selection") {
172
+ if (!coreCandidateSet) {
173
+ const error = new Error("validate_selection called before search_candidates — the federated lineage is missing");
174
+ error.code = "local_core_lineage_missing";
175
+ throw error;
176
+ }
177
+ // 루프의 candidateSet(역치환본)과 보관본의 계보 일치를 다이제스트로 확인한다.
178
+ if (args.candidateSet?.candidateSetDigest !== coreCandidateSet.candidateSetDigest) {
179
+ throw invalid("candidateSet lineage mismatch between the loop and the local Core session");
180
+ }
181
+ let envelope;
182
+ let coreSelection = mapDeep(args.selection, maps.forward);
183
+ try {
184
+ envelope = await core.call(name, {
185
+ workOrder: mapDeep(args.workOrder, maps.forward),
186
+ selection: coreSelection,
187
+ candidateSet: coreCandidateSet,
188
+ });
189
+ } catch (error) {
190
+ /*
191
+ * 반응형 정규화(1회): 리더의 자유 reasonCodes 는 finite 정책에 걸린다
192
+ * (실측: selection_reason_code_not_public_finite). 선택 경계의 finite id
193
+ * 정책은 "public finite reason codes" — opaque 형식 허용 문구가 없어
194
+ * 카탈로그 값으로만 대체한다. reason:host-semantic-judgment 는 관측된
195
+ * 카탈로그 값이며 사실 그 자체다(호스트 LLM 의 의미 판단). 사람이 읽을
196
+ * 원문 사유는 루프의 영수증(selection.assignments 원본)에 이미 남아 있다.
197
+ */
198
+ const issues = error.code === "selection_hub_boundary_rejected" ? boundaryIssues(error) : null;
199
+ const reasonIssues = issues ? issues.filter((issue) => issue.code === "selection_reason_code_not_public_finite") : [];
200
+ if (!reasonIssues.length) throw error;
201
+ const repairedSelection = mapDeep(args.selection, maps.forward);
202
+ for (const assignment of repairedSelection.assignments || []) {
203
+ assignment.reasonCodes = ["reason:host-semantic-judgment"];
204
+ }
205
+ envelope = await core.call(name, {
206
+ workOrder: mapDeep(args.workOrder, maps.forward),
207
+ selection: repairedSelection,
208
+ candidateSet: coreCandidateSet,
209
+ });
210
+ coreSelection = repairedSelection;
211
+ }
212
+ if (!envelope || typeof envelope.selectionValidation !== "object") throw invalid("local Core validation returned no selectionValidation receipt");
213
+ lastValidationEnvelope = envelope;
214
+ lastCoreSelection = coreSelection;
215
+ return mapDeep(envelope.selectionValidation, maps.reverse);
216
+ }
217
+ if (name === "workforce.prepare_execution") {
218
+ if (!lastValidationEnvelope) {
219
+ const error = new Error("prepare_execution called before validate_selection — the federated lineage is missing");
220
+ error.code = "local_core_lineage_missing";
221
+ throw error;
222
+ }
223
+ const envelope = await core.call(name, {
224
+ workOrder: mapDeep(args.workOrder, maps.forward),
225
+ // validate 가 수락한 정확한 본 — 반응형 reasonCode 수리를 반영한다.
226
+ selection: lastCoreSelection || mapDeep(args.selection, maps.forward),
227
+ candidateSet: coreCandidateSet,
228
+ federatedSelection: lastValidationEnvelope,
229
+ validationReceipt: lastValidationEnvelope,
230
+ projectDir,
231
+ });
232
+ if (!envelope || typeof envelope.executionPlan !== "object") throw invalid("local Core preparation returned no executionPlan");
233
+ const plan = mapDeep(envelope.executionPlan, maps.reverse);
234
+ /*
235
+ * 어휘 왕복의 마지막 두 조각 (실측: 루프의 execution_context_mismatch):
236
+ * 1) Core 는 워크오더의 미선언 slot 필드를 스키마 기본값 [] 로 정규화해
237
+ * 돌려준다. 루프의 expectedContext 는 원본 워크오더에서 유도하므로
238
+ * undefined ↔ [] 가 불일치가 된다. 원본에 없던 필드가 빈 값으로 돌아온
239
+ * 경우만 제거한다 — 값이 실제로 다르면 그대로 두어 루프가 잡게 한다.
240
+ * 2) 반응형 reasonCode 수리가 있었으면 Core 어휘의 assignments 에는
241
+ * 카탈로그 값이 들어 있다. 사람이 읽는 원문 사유(루프의 selection)를
242
+ * 자리(slotId+agentReleaseId) 기준으로 복원한다.
243
+ */
244
+ const context = plan.executionContext;
245
+ if (context && Array.isArray(context.slots)) {
246
+ const originalSlots = new Map((args.workOrder.roleSlots || []).map((slot) => [slot.slotId, slot]));
247
+ for (const slot of context.slots) {
248
+ const original = originalSlots.get(slot.slotId);
249
+ if (!original) continue;
250
+ for (const [key, value] of Object.entries(slot)) {
251
+ const emptyDefault = Array.isArray(value) && value.length === 0;
252
+ if (emptyDefault && original[key] === undefined && !["allowedEntityKinds"].includes(key)) {
253
+ delete slot[key];
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (context && Array.isArray(context.assignments)) {
259
+ const originalAssignments = new Map((args.selection.assignments || []).map(
260
+ (assignment) => [`${assignment.slotId}${assignment.agentReleaseId}`, assignment],
261
+ ));
262
+ for (const assignment of context.assignments) {
263
+ const original = originalAssignments.get(`${assignment.slotId}${assignment.agentReleaseId}`);
264
+ if (original) assignment.reasonCodes = original.reasonCodes;
265
+ }
266
+ }
267
+ /*
268
+ * 다이제스트 재서명: Core 의 executionContextDigest 는 Core 어휘(opaque id·
269
+ * 카탈로그 reason·[] 기본값) 위에서 계산됐다. 위의 어휘 왕복으로 루프 어휘
270
+ * 컨텍스트가 됐으므로 루프와 같은 함수로 재계산한다 — 게이트웨이에서의
271
+ * 재서명이지 위조가 아니다: 원본 무결성은 Core 세션과 영수증이 지킨다.
272
+ */
273
+ if (context) {
274
+ const { _test } = require("../agentlas-workforce.cjs");
275
+ plan.executionContextDigest = _test.executionContextDigest(context);
276
+ // roster 행도 같은 이유로 재서명한다 — 역치환이 행 내용(slot 어휘)을
277
+ // 루프 어휘로 바꿨으므로 bundleDigest/executionGraphDigest 를 루프와 같은
278
+ // 함수로 재계산한다.
279
+ for (const row of plan.executionRoster || []) {
280
+ if (row && row.executionGraph && typeof row.executionGraphDigest === "string") {
281
+ row.executionGraphDigest = _test.executionGraphDigest(_test.validateExecutionGraph(row.executionGraph));
282
+ }
283
+ if (row && typeof row.bundleDigest === "string") {
284
+ row.bundleDigest = _test.workforceRuntimeBundleDigest(row);
285
+ }
286
+ }
287
+ }
288
+ return plan;
289
+ }
290
+ const error = new Error(`unsupported local-core workforce tool: ${name}`);
291
+ error.code = "local_core_unsupported_tool";
292
+ throw error;
293
+ }
294
+
295
+ return { callHubTool, close: () => core.close() };
296
+ }
297
+
298
+ module.exports = { createLocalCoreHubTool, SOURCE_SCOPES };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.28",
3
+ "version": "1.0.35",
4
4
  "description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"
@@ -8,7 +8,9 @@
8
8
  "scripts": {
9
9
  "smoke": "sh test/smoke.sh",
10
10
  "test:release-contracts": "npm run smoke",
11
- "sync:architecture": "node scripts/sync-architecture-from-desktop.cjs"
11
+ "sync:architecture": "node scripts/sync-architecture-from-desktop.cjs",
12
+ "vendor:core": "node scripts/vendor-desktop-core.cjs",
13
+ "test:tool-access-notice-parity": "node test/tool-access-notice-parity.cjs"
12
14
  },
13
15
  "engines": {
14
16
  "node": ">=20"