agentlas 0.9.10 → 1.0.2

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 (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /*
3
+ * runtimes/detect — 실행 가능한 런타임 탐지.
4
+ * 데스크탑 electron/runtime/detect.ts 와 동일한 어휘(RuntimeKind)를 쓴다:
5
+ * claude-code | codex | gemini | kimi | grok | cursor | byok | ollama | lmstudio | mlx
6
+ * 여기서는 "실행 파일이 있는가"만 결정론적으로 본다. 모델 응답 여부는 판정하지 않는다
7
+ * (없으면 no_runtime 정직 정지 — 폴백 금지는 상위 계층의 계약).
8
+ */
9
+ const { spawnSync } = require("node:child_process");
10
+
11
+ const RUNTIME_BIN = {
12
+ "claude-code": "claude",
13
+ codex: "codex",
14
+ gemini: "gemini",
15
+ kimi: "kimi",
16
+ grok: "grok",
17
+ cursor: "cursor-agent",
18
+ };
19
+
20
+ const CLI_RUNTIMES = Object.keys(RUNTIME_BIN);
21
+
22
+ function whichSync(bin) {
23
+ const cmd = process.platform === "win32" ? "where" : "which";
24
+ try {
25
+ const res = spawnSync(cmd, [bin], { encoding: "utf8", timeout: 4000 });
26
+ if (res.status === 0) {
27
+ const line = String(res.stdout || "").split(/\r?\n/).find((l) => l.trim());
28
+ return line ? line.trim() : null;
29
+ }
30
+ } catch { /* not found */ }
31
+ return null;
32
+ }
33
+
34
+ /** PATH에서 찾은 CLI 런타임 목록: [{kind, bin, path}] */
35
+ function listAvailableCliRuntimes() {
36
+ const found = [];
37
+ for (const kind of CLI_RUNTIMES) {
38
+ const bin = RUNTIME_BIN[kind];
39
+ const p = whichSync(bin);
40
+ if (p) found.push({ kind, bin, path: p });
41
+ }
42
+ return found;
43
+ }
44
+
45
+ /** 공유 DB의 active_runtime 행(데스크탑이 마지막으로 확정한 런타임). 없으면 null. */
46
+ function activeRuntimeRow(db) {
47
+ try {
48
+ return db.prepare("SELECT kind, backend, source, model, long_context FROM active_runtime WHERE id=1").get() || null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ module.exports = { RUNTIME_BIN, CLI_RUNTIMES, whichSync, listAvailableCliRuntimes, activeRuntimeRow };
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ /*
3
+ * runtimes/overrides — 에이전트별 런타임 오버라이드 (공유 테이블 agent_runtime_overrides).
4
+ *
5
+ * 데스크탑 electron/store/agent-runtime-overrides.ts 와 같은 행 모양을 읽는다:
6
+ * (scope, target_id, label, kind, backend, source, model, effort, long_context, updated_at)
7
+ * 스키마 소유권은 데스크탑에 있으므로 여기서는 방어적으로만 읽는다(tableExists/
8
+ * columnExists) — 구버전 DB에서 열이 없으면 "오버라이드 없음"으로 조용히 전진.
9
+ *
10
+ * 해석 사다리(오너 계약): 명시(--runtime) > 에이전트별 오버라이드 > prefs >
11
+ * active_runtime > detected. resolve.cjs 는 건드리지 않는다 — 이 모듈이
12
+ * 오버라이드 층만 앞에 얹고 나머지는 resolve.cjs 에 위임한다.
13
+ */
14
+ const { tableExists, columnExists } = require("../core/db.cjs");
15
+ const { RUNTIME_BIN, whichSync } = require("./detect.cjs");
16
+ const { resolveRuntime, EXECUTABLE_KINDS } = require("./resolve.cjs");
17
+
18
+ const OVERRIDE_TABLE = "agent_runtime_overrides";
19
+ // 데스크탑 VALID_SCOPES 동형. v2 터미널 호출자는 주로 'agent'지만 firm/division도 읽을 수 있다.
20
+ const VALID_SCOPES = new Set(["agent", "firm", "division"]);
21
+
22
+ function cleanText(value) {
23
+ const trimmed = typeof value === "string" ? value.trim() : "";
24
+ return trimmed ? trimmed : null;
25
+ }
26
+
27
+ /** 방어적 행 → 오버라이드 정규화 (데스크탑 toOverride 동형). */
28
+ function rowToOverride(row) {
29
+ if (!row || !row.kind) return null;
30
+ return {
31
+ scope: row.scope,
32
+ targetId: row.target_id,
33
+ label: cleanText(row.label),
34
+ selection: {
35
+ kind: String(row.kind),
36
+ backend: cleanText(row.backend) || undefined,
37
+ source: cleanText(row.source) || undefined,
38
+ model: cleanText(row.model) || undefined,
39
+ effort: cleanText(row.effort) || undefined,
40
+ longContext: Boolean(row.long_context),
41
+ },
42
+ updatedAt: row.updated_at || null,
43
+ };
44
+ }
45
+
46
+ /** 임의 scope 오버라이드 읽기. 테이블/필수 열이 없으면 null (구버전 DB 전진 호환). */
47
+ function readRuntimeOverride(db, scope, targetId) {
48
+ if (!db || !VALID_SCOPES.has(scope)) return null;
49
+ const id = cleanText(targetId);
50
+ if (!id) return null;
51
+ if (!tableExists(db, OVERRIDE_TABLE)) return null;
52
+ // 필수 열만 확인한다. label/effort 등 부가 열은 SELECT * 결과에서 없으면 undefined로 흡수.
53
+ if (!columnExists(db, OVERRIDE_TABLE, "scope") || !columnExists(db, OVERRIDE_TABLE, "target_id") || !columnExists(db, OVERRIDE_TABLE, "kind")) {
54
+ return null;
55
+ }
56
+ try {
57
+ const row = db.prepare(`SELECT * FROM ${OVERRIDE_TABLE} WHERE scope=? AND target_id=?`).get(scope, id);
58
+ return rowToOverride(row);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /** 에이전트별 오버라이드 (데스크탑 getAgentRuntimeOverride('agent', id) 동형). */
65
+ function readAgentRuntimeOverride(db, agentId) {
66
+ return readRuntimeOverride(db, "agent", agentId);
67
+ }
68
+
69
+ /** 데스크탑 findAgentRuntimeOverride 동형 — targets 우선순위(예: agent > division > firm). */
70
+ function findRuntimeOverride(db, targets) {
71
+ for (const target of Array.isArray(targets) ? targets : []) {
72
+ if (!target || !target.targetId) continue;
73
+ const found = readRuntimeOverride(db, target.scope, target.targetId);
74
+ if (found) return found;
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * 오버라이드를 얹은 런타임 해석.
81
+ * @param {object} p { db, prefs, explicit, agentId, targets?, deps? }
82
+ * targets: [{scope,targetId}] — 주면 agentId 대신 이 우선순위로 오버라이드를 찾는다
83
+ * (firm 경로: agent > division > firm).
84
+ * deps: 테스트 주입 { which, resolve } — 상용 호출자는 사용하지 않는다.
85
+ * @returns resolveRuntime 반환형 + source:"agent-override" 또는
86
+ * { ..., unavailableOverride } (오버라이드가 있으나 이 머신/v2에서 실행 불가 —
87
+ * 조용히 무시하지 않고 사유를 실어 보내며, 호출자가 note를 출력해야 한다)
88
+ */
89
+ function resolveRuntimeForAgent({ db, prefs, explicit, agentId, targets, deps } = {}) {
90
+ const resolveImpl = (deps && deps.resolve) || resolveRuntime;
91
+ // 명시(--runtime)가 항상 이긴다 — 오버라이드는 "사용자가 고르지 않았을 때"의 기본값이다.
92
+ if (explicit) return resolveImpl({ db, prefs, explicit });
93
+
94
+ const override = targets && targets.length
95
+ ? findRuntimeOverride(db, targets)
96
+ : agentId
97
+ ? readAgentRuntimeOverride(db, agentId)
98
+ : null;
99
+ if (override) {
100
+ const kind = override.selection.kind;
101
+ // v2 스트리밍 드라이버가 있는 CLI 런타임만 실제 실행 대상이다. byok/ollama/kimi
102
+ // 오버라이드는 데스크탑에서는 유효하지만 터미널 v2 실행 사다리에는 아직 없다 —
103
+ // 조용히 다른 런타임으로 둔갑시키지 않고 unavailableOverride로 정직하게 알린다.
104
+ if (EXECUTABLE_KINDS.has(kind)) {
105
+ const which = (deps && deps.which) || whichSync;
106
+ const bin = which(RUNTIME_BIN[kind]);
107
+ if (bin) {
108
+ return {
109
+ kind,
110
+ bin,
111
+ model: override.selection.model,
112
+ effort: override.selection.effort,
113
+ source: "agent-override",
114
+ override,
115
+ };
116
+ }
117
+ }
118
+ const resolved = resolveImpl({ db, prefs, explicit: null });
119
+ return { ...resolved, unavailableOverride: override };
120
+ }
121
+ return resolveImpl({ db, prefs, explicit: null });
122
+ }
123
+
124
+ /** 오버라이드 실행 불가 시 사용자에게 출력할 한 줄 (데스크탑 문구 동형). */
125
+ function unavailableOverrideNote(runtime, lang) {
126
+ if (!runtime || !runtime.unavailableOverride) return "";
127
+ const kind = runtime.unavailableOverride.selection.kind;
128
+ return lang === "ko"
129
+ ? `지정 런타임(${kind})을 이 환경에서 실행할 수 없어 기본 런타임(${runtime.kind})으로 실행합니다.`
130
+ : `Assigned runtime (${kind}) is unavailable here — using the default runtime (${runtime.kind}).`;
131
+ }
132
+
133
+ module.exports = {
134
+ readAgentRuntimeOverride,
135
+ readRuntimeOverride,
136
+ findRuntimeOverride,
137
+ resolveRuntimeForAgent,
138
+ unavailableOverrideNote,
139
+ };
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ /*
3
+ * runtimes/resolve — 이번 실행에 쓸 런타임 확정.
4
+ * 사다리: 명시(--runtime) > prefs.runtime(실행 파일 존재 시) > 공유 DB active_runtime >
5
+ * PATH에서 처음 발견된 CLI.
6
+ * 아무것도 없으면 no_runtime "정직 정지" — 키워드/저품질 폴백 금지(오너 결정).
7
+ */
8
+ const { RUNTIME_BIN, whichSync, listAvailableCliRuntimes, activeRuntimeRow } = require("./detect.cjs");
9
+
10
+ // native-host가 스트리밍 드라이버를 갖춘 런타임만 실행 대상으로 삼는다.
11
+ // kimi/grok/cursor 드라이버가 포팅되면 여기에 추가한다 (조용한 오폭 방지).
12
+ const EXECUTABLE_KINDS = new Set(["claude-code", "codex", "gemini"]);
13
+
14
+ class NoRuntimeError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.code = "no_runtime";
18
+ }
19
+ }
20
+
21
+ /**
22
+ * @returns {{kind, bin, model?, source}} source ∈ explicit|prefs|active|detected
23
+ * @throws NoRuntimeError
24
+ */
25
+ function resolveRuntime({ db, prefs, explicit }) {
26
+ if (explicit) {
27
+ const bin = RUNTIME_BIN[explicit];
28
+ if (!bin) throw new NoRuntimeError(`unknown runtime: ${explicit}`);
29
+ if (!EXECUTABLE_KINDS.has(explicit)) {
30
+ throw new NoRuntimeError(`runtime '${explicit}' has no v2 streaming driver yet (available: ${[...EXECUTABLE_KINDS].join(", ")})`);
31
+ }
32
+ const p = whichSync(bin);
33
+ if (!p) throw new NoRuntimeError(`runtime '${explicit}' requested but '${bin}' is not on PATH`);
34
+ return { kind: explicit, bin: p, source: "explicit" };
35
+ }
36
+ const pref = prefs && prefs.runtime;
37
+ if (pref && EXECUTABLE_KINDS.has(pref)) {
38
+ const p = whichSync(RUNTIME_BIN[pref]);
39
+ if (p) return { kind: pref, bin: p, source: "prefs" };
40
+ }
41
+ if (db) {
42
+ const active = activeRuntimeRow(db);
43
+ if (active && EXECUTABLE_KINDS.has(active.kind)) {
44
+ const p = whichSync(RUNTIME_BIN[active.kind]);
45
+ if (p) return { kind: active.kind, bin: p, model: active.model || undefined, source: "active" };
46
+ }
47
+ }
48
+ const found = listAvailableCliRuntimes().filter((r) => EXECUTABLE_KINDS.has(r.kind));
49
+ if (found.length) return { kind: found[0].kind, bin: found[0].path, source: "detected" };
50
+ // 신규 사용자의 최빈 막다른 길: "설치하라"만 있고 방법이 없으면 여기서 이탈한다.
51
+ // 실제 설치 명령을 그대로 준다(데스크탑 온보딩의 "Claude Code 무료로 설치하기"와 동형).
52
+ throw new NoRuntimeError([
53
+ "no_runtime: no agent CLI is connected — Agentlas runs your agents on a CLI you already subscribe to.",
54
+ "",
55
+ "Install one, then rerun:",
56
+ " npm i -g @anthropic-ai/claude-code # Claude Code",
57
+ " npm i -g @openai/codex # Codex CLI",
58
+ " npm i -g @google/gemini-cli # Gemini CLI",
59
+ "",
60
+ "Already installed? Make sure its binary is on PATH (agentlas doctor shows what was detected).",
61
+ ].join("\n"));
62
+ }
63
+
64
+ module.exports = { resolveRuntime, NoRuntimeError, EXECUTABLE_KINDS };
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ /*
3
+ * sessions/apply-fences — 파싱된 펜스 프로토콜을 실제 부작용으로 적용한다.
4
+ * (파싱은 fences.cjs, 적용은 여기 — Desktop client.ts 의 실행 절반에 해당.)
5
+ *
6
+ * 정책 (비협상 불변식과 동일 선상):
7
+ * - memoryEvents: curate.cjs 의 결정적 게이트가 결정한다(큐레이터는 제안만).
8
+ * read 권한 턴은 어떤 durable 쓰기도 없다 — 영수증 이벤트만 남는다.
9
+ * - delegates: 오케스트레이터 동시 상한을 존중한다. 상한 초과는 조용한 큐잉이
10
+ * 아니라 정직한 'delegate-refused' 이벤트다.
11
+ * - automations: 스케줄 검증 실패는 파서에서 이미 거부됐고, 여기서는 next_run_at
12
+ * 계산 불가/권한 부족/division 재귀를 정직하게 거부한다. Desktop 도 division
13
+ * 챗의 자동화 등록을 막는다(자동화가 자동화를 낳는 재귀 방지, client.ts:3493).
14
+ * - asks: 세션 이벤트로만 표면화한다. UI 는 REPL 렌더러 소관.
15
+ */
16
+
17
+ const memoryCurate = require("../memory-cli/curate.cjs");
18
+ const automationStore = require("../automation/store.cjs");
19
+ const schedule = require("../automation/schedule.cjs");
20
+
21
+ /**
22
+ * @param {import('./session.cjs').Session} session 방금 턴을 끝낸 세션
23
+ * @param {object} parsed parseReplyFences 결과
24
+ * @param {{orch?: object}} opts orch 미지정 시 session.orchestrator 사용
25
+ * @returns 적용 영수증 { asks, memory, automations, delegates, refused }
26
+ */
27
+ function applyReplyFences(session, parsed, opts = {}) {
28
+ const orch = opts.orch || session.orchestrator || null;
29
+ const record = (ev) => session._record({ at: Date.now(), ...ev });
30
+ const receipts = { asks: 0, memory: null, automations: [], delegates: [], refused: [] };
31
+ const canWrite = session.permission !== "read";
32
+
33
+ // 파서가 표면화한 오류(잘못된 스케줄 등) — 조용히 삼키지 않는다.
34
+ for (const err of parsed.errors || []) {
35
+ record({ type: "fence-error", text: err });
36
+ }
37
+
38
+ /* ── asks → 세션 이벤트 (REPL 렌더러가 표면화) ───────────────────────── */
39
+ for (const ask of parsed.asks || []) {
40
+ record({ type: "ask", payload: ask });
41
+ receipts.asks += 1;
42
+ }
43
+
44
+ /* ── memoryEvents → curate 게이트 (제안 ≠ 승인) ─────────────────────── */
45
+ if (Array.isArray(parsed.memoryEvents) && parsed.memoryEvents.length) {
46
+ // 프로젝트 경계: 명시 초기화된(.agentlas 존재) 폴더만 project 스코프 대상 —
47
+ // 임의 cwd 에 .agentlas 스캐폴딩을 만들지 않는다(session.cjs 프롬프트 증강과 동일 기준).
48
+ let projectPath = null;
49
+ try {
50
+ const fs = require("node:fs");
51
+ const path = require("node:path");
52
+ projectPath = fs.existsSync(path.join(session.cwd, ".agentlas")) ? session.cwd : null;
53
+ } catch { projectPath = null; }
54
+ const ctx = {
55
+ permission: session.permission,
56
+ projectPath,
57
+ agentId: session.agent.id,
58
+ curatedMemories: [],
59
+ };
60
+ // 게이트 로직을 복제하지 않기 위해 이벤트를 정확한 wire 블록으로 재구성해
61
+ // curateCliReply 에 그대로 통과시킨다(시크릿/스코프/중복/권한 게이트 전부 재사용).
62
+ const heading = memoryCurate.loadArch().eventsHeading;
63
+ const block = `${heading}\n\`\`\`json\n${JSON.stringify(parsed.memoryEvents)}\n\`\`\``;
64
+ try {
65
+ memoryCurate.curateCliReply(session.db, block, ctx);
66
+ } catch { /* 게이트 실패가 턴 자체를 죽이면 안 된다 */ }
67
+ receipts.memory = {
68
+ candidates: parsed.memoryEvents.length,
69
+ written: ctx.curatedMemories.length,
70
+ permission: session.permission,
71
+ };
72
+ // read 권한 턴 = durable 쓰기 0 — 영수증 이벤트만 남는다.
73
+ record({ type: "memory-curated", ...receipts.memory });
74
+ }
75
+
76
+ /* ── automations → automation/store.cjs addAutomation ────────────────── */
77
+ for (const a of parsed.automations || []) {
78
+ if (!canWrite) {
79
+ // Desktop automationPermissionRequired(client.ts:3500) 과 동일한 정직 거부.
80
+ const refusal = { type: "automation-refused", name: a.name, reason: "write permission required" };
81
+ record(refusal);
82
+ receipts.refused.push(refusal);
83
+ continue;
84
+ }
85
+ if (session.parent || session.chatKind === "division") {
86
+ // division 서브세션(자동화 marker 세션 포함)의 자동화 등록 금지 — 자동화가
87
+ // 자동화를 만드는 재귀 방지. 데스크탑 client.ts:3493(chat.kind !== 'division')과 동형:
88
+ // parent 없는 자동화 실행 세션도 division 챗이므로 반드시 이 가드에 걸려야 한다.
89
+ const refusal = { type: "automation-refused", name: a.name, reason: "division session may not register automations" };
90
+ record(refusal);
91
+ receipts.refused.push(refusal);
92
+ continue;
93
+ }
94
+ // 파서가 이미 스케줄을 검증했지만, next_run_at 이 실제로 계산되는지 최종 확인.
95
+ // 시계 없는 행(next_run_at NULL)은 앱 스케줄러가 영영 안 깨운다 — 등록 거부가 정직.
96
+ const next = schedule.nextAutomationRun({ schedule: a.schedule, timezone: a.tz || null });
97
+ if (!next) {
98
+ const refusal = { type: "automation-refused", name: a.name, reason: `schedule "${a.schedule}" has no next run` };
99
+ record(refusal);
100
+ receipts.refused.push(refusal);
101
+ continue;
102
+ }
103
+ try {
104
+ const id = automationStore.addAutomation(session.db, {
105
+ name: a.name,
106
+ targetType: "agent",
107
+ targetId: session.agent.id, // 실행 주체 = 이 세션의 에이전트
108
+ cron: a.schedule, // 레거시 미러 토큰(schedule 열) — daemon 이 legacyScheduleSpec 으로 해석
109
+ prompt: a.prompt,
110
+ tz: a.tz || null,
111
+ }, next);
112
+ const receipt = {
113
+ type: "automation-registered",
114
+ id,
115
+ name: a.name,
116
+ schedule: a.schedule,
117
+ nextRunAt: next.toISOString(),
118
+ // steps[] 는 wire 로 받았지만 터미널은 그래프 합성이 없다 — 위장하지 않고 표기.
119
+ ...(a.steps ? { stepsIgnored: a.steps.length } : {}),
120
+ };
121
+ record(receipt);
122
+ receipts.automations.push(receipt);
123
+ } catch (e) {
124
+ const refusal = { type: "automation-refused", name: a.name, reason: (e && e.message) || String(e) };
125
+ record(refusal);
126
+ receipts.refused.push(refusal);
127
+ }
128
+ }
129
+
130
+ /* ── delegates → 오케스트레이터로 division 서브세션 스폰 ─────────────── */
131
+ // 상한은 lazy require — session.cjs ↔ orchestrator.cjs 로드 사이클 방지.
132
+ const { maxParallel } = require("./orchestrator.cjs");
133
+ for (const d of parsed.delegates || []) {
134
+ if (!orch || !session.key) {
135
+ const refusal = { type: "delegate-refused", target: d.target, reason: "no orchestrator attached to this session" };
136
+ record(refusal);
137
+ receipts.refused.push(refusal);
138
+ continue;
139
+ }
140
+ const brief = (d.brief || "").trim();
141
+ if (!brief) {
142
+ const refusal = { type: "delegate-refused", target: d.target, reason: "empty delegate brief" };
143
+ record(refusal);
144
+ receipts.refused.push(refusal);
145
+ continue;
146
+ }
147
+ if (orch.runningCount() >= maxParallel()) {
148
+ // 상한 초과 = 대기가 아니라 정직한 거부(오케스트레이터 정책과 동일).
149
+ const refusal = {
150
+ type: "delegate-refused",
151
+ target: d.target,
152
+ reason: `parallel limit ${maxParallel()} reached (running: ${orch.runningCount()})`,
153
+ };
154
+ record(refusal);
155
+ receipts.refused.push(refusal);
156
+ continue;
157
+ }
158
+ // 부모의 런타임/권한/작업폴더를 물려받는 division 자식. spawnImpl/timeoutConfig
159
+ // 전달은 계약 테스트(오프라인 fake spawn)를 위해 필요하고 프로덕션에선 null 이다.
160
+ const child = orch.spawn({
161
+ agent: session.agent,
162
+ runtime: session.runtime,
163
+ permission: session.permission,
164
+ cwd: session.cwd,
165
+ parentKey: session.key,
166
+ activate: false,
167
+ title: `delegate: ${d.target}`,
168
+ spawnImpl: session._spawnImpl,
169
+ timeoutConfig: session._timeoutConfig,
170
+ });
171
+ try {
172
+ const promise = orch.sendTo(child.key, brief);
173
+ const receipt = { type: "delegate-spawned", key: child.key, target: d.target, allocation: d.allocation || null };
174
+ record(receipt);
175
+ receipts.delegates.push({ key: child.key, target: d.target, promise });
176
+ } catch (e) {
177
+ // sendTo 의 상한 판정이 최종 권위다(스폰 사이 레이스) — 스폰만 된 유령 세션은 제거.
178
+ orch.remove(child.key);
179
+ const refusal = { type: "delegate-refused", target: d.target, reason: (e && e.message) || String(e) };
180
+ record(refusal);
181
+ receipts.refused.push(refusal);
182
+ }
183
+ }
184
+
185
+ return receipts;
186
+ }
187
+
188
+ module.exports = { applyReplyFences };