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,218 @@
1
+ "use strict";
2
+ /*
3
+ * storm/storm — Agentlas 자체 Goal/UltraCode 하네스 (Stormbreaker).
4
+ *
5
+ * v1 engine/agentlas-parity.cjs (legacy-v1-engine-snapshot 608–751행)의 포팅.
6
+ * plan → allocate → execute → verify: Hephaestus `route`로 라우팅 근거만 수집하고,
7
+ * 실행은 storm/swarm.cjs 의 스웜 하네스(stormbreaker: true)로 돌린다.
8
+ *
9
+ * 불변식 (v1 사고에서 배운 것 — 약화 금지):
10
+ * - Core owns the exact Goal/UltraCode prompt. Terminal supplies only host
11
+ * runtime inventory, worker context, and execution; no local prompt fallback.
12
+ * Core 하네스가 없으면 stormbreaker-core-harness-unavailable 로 정직 정지한다 —
13
+ * 로컬 모조 하네스로 흉내내지 않는다.
14
+ * - Hephaestus CLI `--auto-run`을 절대 쓰지 않는다: storm은 자기 하네스에서
15
+ * 실행한다. route 출력은 advisory 증거일 뿐이고 Agentlas 부모 플래너가 최종
16
+ * 계획을 소유한다.
17
+ * - route 실패(비JSON/exit≠0)는 경고 후 원 목표로 계속한다 — route는 선택적
18
+ * 증거이지 실행 게이트가 아니다.
19
+ */
20
+ const os = require("node:os");
21
+ const path = require("node:path");
22
+ const fs = require("node:fs");
23
+ const { spawn } = require("node:child_process");
24
+ const { Ui } = require("../agentlas-ui.cjs");
25
+ const {
26
+ loadCoreStormbreakerHarness,
27
+ resolveCoreRuntimeRoot,
28
+ spawnCoreModule,
29
+ } = require("../agentlas-core-harness.cjs");
30
+
31
+ /**
32
+ * create(deps) — v1 parity 팩토리와 동일한 D-주입 생성.
33
+ * storm은 swarm과 같은 D를 공유한다 (storm/deps.cjs buildStormDeps 참조).
34
+ */
35
+ function create(deps) {
36
+ const D = deps;
37
+ const { swarmRun } = require("./swarm.cjs").create(D);
38
+
39
+ function newUi(lang) {
40
+ return new Ui({ lang: lang || D.prefsLang() });
41
+ }
42
+
43
+ // ── Hephaestus 런타임 해석 (설치 런처 우선, Core 파이썬 루트 폴백) ──
44
+ function hephaestusBin() {
45
+ const candidates = [
46
+ process.env.HEPHAESTUS_BIN,
47
+ path.join(os.homedir(), ".agentlas", "runtime", "current", "bin", "hephaestus"),
48
+ ];
49
+ for (const c of process.platform === "win32" ? [] : candidates) {
50
+ try {
51
+ if (c && fs.existsSync(c)) {
52
+ fs.accessSync(c, fs.constants.X_OK);
53
+ return { kind: "bin", exec: c };
54
+ }
55
+ } catch { /* 다음 후보 */ }
56
+ }
57
+ const root = resolveCoreRuntimeRoot();
58
+ if (root) return { kind: "python", root };
59
+ return null;
60
+ }
61
+
62
+ function spawnHephaestus(args, opts) {
63
+ const found = hephaestusBin();
64
+ if (!found) return null;
65
+ if (found.kind === "bin") return spawn(found.exec, args, opts);
66
+ return spawnCoreModule("agentlas_cloud", args, opts, found.root);
67
+ }
68
+
69
+ // ── storm — Agentlas 자체 Goal/UltraCode 하네스 ──
70
+ // Core owns the exact Goal/UltraCode prompt. Terminal supplies only host
71
+ // runtime inventory, worker context, and execution; no local prompt fallback.
72
+ // ctx: { ui?, cwd?, research?, background?, runtimeOverride? }
73
+ async function stormRun(db, goal, ctx = {}) {
74
+ const ui = ctx.ui || newUi();
75
+ goal = String(goal || "").trim();
76
+ if (!goal) {
77
+ ui.warn("usage: storm <goal> [--research]");
78
+ return { ok: false };
79
+ }
80
+ if (goal.startsWith("-")) {
81
+ ui.error("goal cannot start with '-'.");
82
+ return { ok: false };
83
+ }
84
+ const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd());
85
+ let executionHarness;
86
+ try {
87
+ executionHarness = await loadCoreStormbreakerHarness(cwd);
88
+ } catch (error) {
89
+ ui.error(`Stormbreaker Core harness unavailable: ${String((error && error.message) || error).slice(0, 400)}`);
90
+ return { ok: false, error: "stormbreaker-core-harness-unavailable" };
91
+ }
92
+ const args = ["route", goal, "--project", cwd, "--runtime", "terminal"];
93
+ if (ctx.research) args.push("--research-evidence");
94
+ if (ctx.background) {
95
+ ui.warn(ui.lang === "ko"
96
+ ? "Agentlas 자체 Stormbreaker 하네스는 현재 포그라운드에서 실행합니다. 세션이 끝나도 영수증으로 재개 지점을 보존합니다."
97
+ : "The Agentlas-owned Stormbreaker harness currently runs in the foreground and preserves resume receipts.");
98
+ }
99
+
100
+ let result = { code: 0, stdout: "", stderr: "" };
101
+ if (hephaestusBin()) {
102
+ ui.beginTurn();
103
+ ui.startSpinner(ui.lang === "ko" ? "Stormbreaker 라우팅 근거 수집 중…" : "Stormbreaker gathering route evidence…");
104
+ result = await new Promise((resolve) => {
105
+ const child = spawnHephaestus(args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
106
+ let stdout = "";
107
+ let stderrTail = [];
108
+ child.stdout.on("data", (c) => { stdout += c.toString(); });
109
+ child.stderr.on("data", (c) => {
110
+ for (const ln of c.toString().split("\n")) {
111
+ const line = ln.trim();
112
+ if (!line) continue;
113
+ stderrTail.push(line);
114
+ if (stderrTail.length > 30) stderrTail.shift();
115
+ ui.updateSpinner(line.slice(0, 100));
116
+ }
117
+ });
118
+ child.on("error", (err) => resolve({ code: 1, stdout, stderr: String(err.message) }));
119
+ child.on("close", (code) => resolve({ code: code ?? 0, stdout, stderr: stderrTail.join("\n") }));
120
+ });
121
+ ui.stopSpinner();
122
+ ui.endTurn();
123
+ }
124
+
125
+ let json = null;
126
+ try {
127
+ const s = result.stdout.indexOf("{");
128
+ const e = result.stdout.lastIndexOf("}");
129
+ if (s >= 0 && e > s) json = JSON.parse(result.stdout.slice(s, e + 1));
130
+ } catch { /* 비JSON 출력 */ }
131
+
132
+ let routeContext = "";
133
+ if (json) {
134
+ const action = json.action || json.route_action || (json.route_decision && json.route_decision.action) || json.status || "?";
135
+ ui.line("");
136
+ ui.ok((ui.lang === "ko" ? "storm 결과: " : "storm result: ") + action);
137
+ const fields = {
138
+ receipt_id: json.receipt_id || (json.route_decision && json.route_decision.receipt_id),
139
+ pipeline_id: json.pipeline_id,
140
+ journal: json.journal,
141
+ status: json.status,
142
+ can_report_success: json.final_gate && json.final_gate.can_report_success,
143
+ };
144
+ if (json.auto_run) {
145
+ fields.auto_run = String(json.auto_run.status || "") + (json.auto_run.reason ? ` — ${json.auto_run.reason}` : "");
146
+ }
147
+ const sel = json.selected;
148
+ if (sel) fields.selected = typeof sel === "string" ? sel : sel.id || sel.slug || sel.name;
149
+ for (const [k, v] of Object.entries(fields)) {
150
+ if (v !== undefined && v !== null && v !== "") ui.info(`${k}: ${v}`);
151
+ }
152
+ if (json.clarify_question) ui.warn(String(json.clarify_question));
153
+ if (json.reason) ui.info(String(json.reason));
154
+ // 파이프라인 패킷 요약
155
+ const packets = json.execution_fabric && json.execution_fabric.packets;
156
+ if (Array.isArray(packets)) {
157
+ routeContext = packets.slice(0, 24).map((p) => {
158
+ const title = String(p.title || p.id || "packet").replace(/\s+/g, " ").slice(0, 160);
159
+ const card = p.card ? ` [agent:${String(p.card).slice(0, 100)}]` : "";
160
+ return `- ${title}${card}`;
161
+ }).join("\n");
162
+ for (const p of packets.slice(0, 12)) {
163
+ ui.line(" " + ui.c.emerald("▸ ") + ui.c.text(String(p.title || p.id || "packet")) + (p.card ? ui.c.dim(" " + p.card) : ""));
164
+ }
165
+ }
166
+ // 파이프라인이 아니면 추천 에이전트라도 보여준다 (hub_candidates 등)
167
+ const exec = json.execution || {};
168
+ const recos = []
169
+ .concat(exec.recommended_agents || [], exec.alternatives || [])
170
+ .map((a) => (typeof a === "string" ? a : a && (a.id || a.slug || a.name)))
171
+ .filter(Boolean);
172
+ if (recos.length) {
173
+ ui.line("");
174
+ ui.info(ui.lang === "ko" ? "추천 에이전트:" : "recommended agents:");
175
+ for (const r of recos.slice(0, 8)) ui.line(" " + ui.c.emerald("▸ ") + ui.c.text(r));
176
+ ui.info(ui.lang === "ko" ? '빌려 실행: agentlas cloud install <slug> 또는 "/storm"을 더 구체적 목표로.' : "borrow: agentlas cloud install <slug>, or re-run /storm with a more specific goal.");
177
+ }
178
+ } else {
179
+ const raw = (result.stdout || result.stderr || "").trim();
180
+ if (raw) ui.markdown(raw.slice(0, 4000));
181
+ }
182
+ if (result.code !== 0 && !json) {
183
+ ui.warn(`Hephaestus route evidence unavailable (${result.code}); Agentlas parent planner will continue from the original goal.`);
184
+ }
185
+
186
+ const harnessResult = await swarmRun(db, goal, {
187
+ ...ctx,
188
+ ui,
189
+ cwd,
190
+ runtimeOverride: ctx.runtimeOverride,
191
+ stormbreaker: true,
192
+ executionHarness,
193
+ routeContext,
194
+ });
195
+ return { ...harnessResult, routeDecision: json };
196
+ }
197
+
198
+ async function cmdStorm(db, args, runtimeOverride, executionContext = {}) {
199
+ const rest = [];
200
+ const ctx = {
201
+ ...executionContext,
202
+ cwd: executionContext.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd()),
203
+ runtimeOverride,
204
+ };
205
+ for (let i = 0; i < args.length; i++) {
206
+ if (args[i] === "--research" || args[i] === "--research-evidence") ctx.research = true;
207
+ else if (args[i] === "--background") ctx.background = true;
208
+ else rest.push(args[i]);
209
+ }
210
+ const r = await stormRun(db, rest.join(" "), ctx);
211
+ if (!r.ok) process.exitCode = 1;
212
+ return r;
213
+ }
214
+
215
+ return { stormRun, cmdStorm };
216
+ }
217
+
218
+ module.exports = { create };
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ /*
3
+ * storm/swarm — emergent 에이전트 스웜 (블랙보드 + `## Spawn` 그래프 성장 + 종합).
4
+ *
5
+ * v1 engine/agentlas-parity.cjs (legacy-v1-engine-snapshot 839–1181행)의 포팅.
6
+ * 앱 mcp/swarm-run.ts 프로토콜의 CLI 구현이며, 다음 계약은 토씨까지 보존한다
7
+ * (test/workload-routing-contract.cjs 가 소스 리터럴로 고정):
8
+ *
9
+ * - `## Spawn` 블록 프로토콜: 워커는 결과 본문 아래 `## Spawn` 블록으로만 자식
10
+ * 작업을 제안한다. 각 자식은 상위 AI가 저작한 workload allocation JSON 하나다.
11
+ * - LIVE_RUNTIME_INVENTORY 계약: 워커가 고를 수 있는 runtimeId/exactModelId는
12
+ * 실제 설치·연결된 인벤토리뿐이다. 호스트는 그 선택을 *검증만* 한다 —
13
+ * 할당을 직접 저작하거나 키워드로 재구성하지 않는다(오너 결정).
14
+ * - fail-closed 할당: resolveAllocationAcrossRuntimes가 거부하면 CLI 기본 모델로
15
+ * 조용히 실행하지 않고 "model allocation failed closed"로 던진다.
16
+ * - Stormbreaker 모드는 Agentlas Core의 정본 Goal+UltraCode 하네스 프롬프트만
17
+ * 사용한다. 이 파일은 GOAL/ULTRACODE 모드 프롬프트를 절대 로컬 정의하지 않는다.
18
+ * - 모든 워커는 engine/workforce/capture.cjs 의 headless 캡처(captureRuntime)
19
+ * 또는 runApi 로만 실행된다 — 제2의 bespoke 스폰 경로 금지.
20
+ * - 할당 영수증(JSONL)에는 원문 프롬프트/작업 텍스트가 남지 않는다
21
+ * (createDecisionReceipt가 해시·리댁션 — workload-routing 계약 테스트가 고정).
22
+ */
23
+ const { Ui } = require("../agentlas-ui.cjs");
24
+ const workloadRouting = require("../agentlas-workload-routing.cjs");
25
+
26
+ // ── 스웜 상수 (앱 mcp/swarm-run.ts 와 동일한 안전 상한) ──
27
+ const SWARM_MAX_TASKS = 24;
28
+ const SWARM_SPAWN_PER_TURN = 12;
29
+
30
+ // ── swarm 프로토콜 프롬프트 — 앱 swarm-run.ts 와 동일 구조 ──
31
+ // "WORK ALREADY ASSIGNED TO PEERS" 블록은 형제 소유권 가시화 계약이다: 워커가
32
+ // 이미 배정된 패킷을 중복 spawn 해 사용자의 돈을 낭비하는 사고를 막는다.
33
+ function swarmProtocol(goal, tasks, task, liveRuntimeInventory) {
34
+ const doneList = tasks
35
+ .filter((t) => t.status === "done")
36
+ .slice(-8)
37
+ .map((t) => `- ${t.title}`)
38
+ .join("\n");
39
+ const assignedList = tasks
40
+ .filter((t) => t.id !== task.id && t.status !== "failed")
41
+ .slice(0, 24)
42
+ .map((t) => `- [${t.status}] ${t.title}${t.brief ? ` — ${t.brief}` : ""}`)
43
+ .join("\n");
44
+ return [
45
+ "You are one worker in an EMERGENT AGENT SWARM collaborating on a shared goal.",
46
+ `SHARED GOAL: ${goal}`,
47
+ "",
48
+ "YOUR TASK RIGHT NOW:",
49
+ `- ${task.title}${task.role ? ` (role: ${task.role})` : ""}`,
50
+ task.brief ? `- Details: ${task.brief}` : "",
51
+ "",
52
+ doneList ? `Already completed by peers (recent):\n${doneList}` : "No peer results yet — you may be first.",
53
+ assignedList ? `WORK ALREADY ASSIGNED TO PEERS (never duplicate these packets):\n${assignedList}` : "",
54
+ "",
55
+ "RULES:",
56
+ "1. Do your task concretely with available tools/files in the current working folder.",
57
+ `LIVE_RUNTIME_INVENTORY=${JSON.stringify(liveRuntimeInventory || [])}`,
58
+ "2. If the goal needs MORE work beyond your task — split into concrete next steps — end your",
59
+ " message with a `## Spawn` block. Every child MUST be one JSON object with a higher-level AI allocation. Choose runtimeId and exactModelId only from LIVE_RUNTIME_INVENTORY:",
60
+ " ## Spawn",
61
+ ' - {"role":"webmaster","brief":"build the landing page structure","allocation":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-1","exactModelId":"model-from-inventory","tier":"balanced","effort":"high","phase":"delegate","reasonCodes":["complex-reasoning"],"rationale":"requires coordinated implementation","requiredCapabilities":["code","tools"]}}',
62
+ ' - {"brief":"run focused tests","allocation":{"schema":"agentlas.workload-allocation.v1","runtimeId":"runtime-2","exactModelId":"model-from-inventory","tier":"economy","effort":"low","phase":"delegate","reasonCodes":["bounded-scope"],"rationale":"bounded verification","requiredCapabilities":["code","tools"]}}',
63
+ " Choose each exact runtime/model and effort from the actual child difficulty; do not copy one allocation to every child.",
64
+ " (role is optional. Do NOT spawn if the goal is already met or another pending/running/done packet already owns that work.)",
65
+ "3. Do NOT restate the whole goal. Do NOT invent work that isn't needed — over-spawning wastes the user's money.",
66
+ "4. Everything above the `## Spawn` block is your result and is shared with peers on the blackboard.",
67
+ ]
68
+ .filter(Boolean)
69
+ .join("\n");
70
+ }
71
+
72
+ // `## Spawn` 블록 파서 — 순수 함수 (test/swarm-protocol-contract.cjs 가 고정).
73
+ function parseSwarmOutput(text) {
74
+ const m = String(text).match(/^[ \t]*##[ \t]*Spawn[ \t]*$/im);
75
+ if (!m || m.index === undefined) return { result: String(text).trim(), spawn: [] };
76
+ const result = String(text).slice(0, m.index).trim();
77
+ const block = String(text).slice(m.index + m[0].length).split("\n");
78
+ const spawn = [];
79
+ for (const raw of block) {
80
+ const line = raw.trim();
81
+ if (!line.startsWith("-")) {
82
+ if (line.startsWith("#")) break;
83
+ continue;
84
+ }
85
+ const body = line.replace(/^-\s*/, "");
86
+ if (body.startsWith("{")) {
87
+ const item = workloadRouting.extractJsonObject(body);
88
+ if (item && typeof item === "object" && !Array.isArray(item)) {
89
+ const brief = String(item.brief || "").trim();
90
+ const allocation = workloadRouting.normalizeAllocation(item.allocation || item, "delegate");
91
+ if (brief) {
92
+ spawn.push({
93
+ title: String(item.title || brief).trim().slice(0, 80),
94
+ brief: brief.slice(0, 8_000),
95
+ role: item.role ? String(item.role).trim().slice(0, 80) : undefined,
96
+ allocation,
97
+ });
98
+ }
99
+ }
100
+ if (spawn.length >= SWARM_SPAWN_PER_TURN) break;
101
+ continue;
102
+ }
103
+ const parts = body.split("|");
104
+ let role;
105
+ let brief;
106
+ if (parts.length >= 2) {
107
+ role = parts[0].trim() || undefined;
108
+ brief = parts.slice(1).join("|").trim();
109
+ } else {
110
+ brief = body.trim();
111
+ }
112
+ // Legacy text remains parseable, but it has no AI-authored allocation and
113
+ // therefore runs on the current model with an observable fallback receipt.
114
+ if (brief) spawn.push({ title: brief.slice(0, 80), brief, role, allocation: null });
115
+ if (spawn.length >= SWARM_SPAWN_PER_TURN) break;
116
+ }
117
+ return { result, spawn };
118
+ }
119
+
120
+ /**
121
+ * create(deps) — v1 parity 팩토리와 동일한 D-주입 생성.
122
+ * D 멤버: prefsLang, resolveRuntime, listAvailableRuntimes, captureRuntime,
123
+ * runApi, buildChildEnvCli, projectCwd, runCwd, modelRoutingReceiptPath.
124
+ * (test/workload-routing-contract.cjs 가 이 D 계약으로 스텁 주입해 end-to-end를
125
+ * 검증한다 — 멤버 이름/시그니처를 바꾸면 데스크탑 패리티가 깨진다.)
126
+ */
127
+ function create(deps) {
128
+ const D = deps;
129
+
130
+ function newUi(lang) {
131
+ return new Ui({ lang: lang || D.prefsLang() });
132
+ }
133
+
134
+ // ctx: { ui?, cwd?, permission?, runtime?, runtimeOverride?, concurrency?, agent?, projectPath? }
135
+ async function swarmRun(db, goal, ctx = {}) {
136
+ const ui = ctx.ui || newUi();
137
+ goal = String(goal || "").trim();
138
+ if (!goal) {
139
+ ui.warn("usage: swarm <goal> [--parallel N]");
140
+ return { ok: false };
141
+ }
142
+ // 런타임 부재는 D.resolveRuntime의 code="no_runtime" throw로 정직 정지한다.
143
+ const runtime = ctx.runtime || D.resolveRuntime(db, ctx.runtimeOverride);
144
+ const stormbreaker = ctx.stormbreaker === true;
145
+ const executionHarness = stormbreaker ? ctx.executionHarness : null;
146
+ // Core 하네스 없이 stormbreaker 모드 진입 금지 — 로컬 모조 프롬프트로 대체하는
147
+ // 것은 계약 위반이다(모델 실행 전에 실패해야 한다).
148
+ if (stormbreaker && (!executionHarness || typeof executionHarness.system_prompt !== "string")) {
149
+ ui.error("Stormbreaker requires the canonical Goal + UltraCode harness from Agentlas Core.");
150
+ return { ok: false, error: "stormbreaker-core-harness-unavailable" };
151
+ }
152
+ const coreHarnessPrompt = executionHarness && executionHarness.system_prompt;
153
+ const permission = ctx.permission || "write";
154
+ const discoveredRuntimes = ctx.runtimes && ctx.runtimes.length
155
+ ? ctx.runtimes
156
+ : typeof D.listAvailableRuntimes === "function"
157
+ ? D.listAvailableRuntimes(db, runtime)
158
+ : [runtime];
159
+ const runtimes = discoveredRuntimes
160
+ .map((candidate, index) => ({ ...candidate, runtimeId: candidate.runtimeId || `runtime-${index + 1}` }));
161
+ const liveRuntimeInventory = workloadRouting.runtimeInventory(runtimes);
162
+ // 워커는 사용자의 실제 프로젝트 폴더에서 실행된다 (계약 테스트가 소스 고정).
163
+ const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd());
164
+ const concurrency = Math.max(1, Math.min(8, Number(ctx.concurrency) || 3));
165
+ const env = await D.buildChildEnvCli(db, {
166
+ projectPath: ctx.projectPath || null,
167
+ agentId: ctx.agent && ctx.agent.id,
168
+ permission,
169
+ cwd,
170
+ lang: ui.lang,
171
+ });
172
+
173
+ async function runBaseWorker(system, prompt, options = {}) {
174
+ // 플래너/판정성 턴은 텍스트(JSON)만 내면 된다 — 실행 권한을 주면 모델이
175
+ // 계획 대신 goal을 그 자리에서 실행해버린다(실사용 storm 테스트에서 실증:
176
+ // 플래너 턴이 hello.txt를 직접 생성). options.permission으로 read 강등 허용.
177
+ const turnPermission = options.permission || permission;
178
+ if (runtime.mode === "cli") {
179
+ return await D.captureRuntime(runtime.kind, system, prompt, {
180
+ cwd,
181
+ env,
182
+ permission: turnPermission,
183
+ // no-authority: 도구 자체를 비운다 — read(plan 모드)만으로는 claude가
184
+ // "계획 파일 작성 후 승인 대기" UX로 납치되어 JSON을 내지 않는다(실측).
185
+ authorityMode: options.authorityMode,
186
+ model: ctx.modelPin || runtime.model || null,
187
+ effort: ctx.effortPin === undefined ? null : ctx.effortPin,
188
+ });
189
+ }
190
+ const text = await D.runApi(runtime.backend, ctx.modelPin || runtime.model, system, prompt);
191
+ return typeof text === "string" ? text : (text && text.text) || "";
192
+ }
193
+
194
+ function recordAllocation(task, stage, decision, resolution, parentTaskId = null) {
195
+ const receipt = workloadRouting.createDecisionReceipt({
196
+ taskId: `${stage}-${task.id || "synthesis"}`,
197
+ parentTaskId,
198
+ taskText: task.brief || task.title || goal,
199
+ stage,
200
+ decision,
201
+ resolution,
202
+ });
203
+ try {
204
+ workloadRouting.appendDecisionReceipt(
205
+ receipt,
206
+ ctx.receiptFile || (D.modelRoutingReceiptPath && D.modelRoutingReceiptPath()),
207
+ );
208
+ } catch (error) {
209
+ ui.warn(`model routing receipt failed: ${String((error && error.message) || error).slice(0, 120)}`);
210
+ }
211
+ return receipt;
212
+ }
213
+
214
+ async function runAllocatedWorker(system, prompt, task, stage, parentTaskId = null) {
215
+ const resolution = workloadRouting.resolveAllocationAcrossRuntimes({
216
+ runtimes,
217
+ fallbackRuntime: runtime,
218
+ decision: task.allocation,
219
+ modelPin: ctx.modelPin,
220
+ effortPin: ctx.effortPin,
221
+ availableModels: ctx.availableModels,
222
+ maxTier: ctx.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
223
+ });
224
+ recordAllocation(task, stage, task.allocation, resolution, parentTaskId);
225
+ // 할당 거부 후 CLI 기본 모델로 조용히 실행 금지 — fail-closed (계약 테스트 고정).
226
+ if (!resolution.ok) {
227
+ throw new Error(`model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
228
+ }
229
+ if (resolution.fallbackReason) {
230
+ ui.info(`model route: ${resolution.source} · ${resolution.runtimeId || "current"} · ${resolution.model || runtime.kind || runtime.backend} · ${resolution.fallbackReason}`);
231
+ }
232
+ const selectedRuntime = resolution.runtime || runtime;
233
+ task.resolvedAllocation = {
234
+ runtimeId: resolution.runtimeId || selectedRuntime.runtimeId || null,
235
+ runtimeKind: selectedRuntime.kind || selectedRuntime.backend || null,
236
+ model: resolution.model || selectedRuntime.model || null,
237
+ effort: resolution.effort ?? null,
238
+ source: resolution.source,
239
+ fallbackReason: resolution.fallbackReason || null,
240
+ };
241
+ if (selectedRuntime.mode === "cli") {
242
+ return await D.captureRuntime(selectedRuntime.kind, system, prompt, {
243
+ cwd,
244
+ env,
245
+ permission,
246
+ model: resolution.model,
247
+ effort: resolution.effort,
248
+ });
249
+ }
250
+ const text = await D.runApi(selectedRuntime.backend, resolution.model || selectedRuntime.model, system, prompt);
251
+ return typeof text === "string" ? text : (text && text.text) || "";
252
+ }
253
+
254
+ const label = runtime.mode === "cli" ? runtime.kind : runtime.backend;
255
+ ui.line("");
256
+ ui.line(ui.c.paw("◤ ") + ui.c.bold(ui.c.text(stormbreaker ? "stormbreaker" : "swarm")) + ui.c.dim(` Agentlas harness · ${label} · x${concurrency} · max ${SWARM_MAX_TASKS} tasks`));
257
+ ui.info(goal.slice(0, 120));
258
+
259
+ ui.startSpinner(ui.lang === "ko" ? "상위 AI가 작업별 모델 비용을 배정 중…" : "Higher-level AI is allocating task models…");
260
+ let planned = null;
261
+ // 플래너는 읽기 전용 + 무효 JSON 1회 재시도. v1의 "현재 모델로 투명 폴백" 경로는
262
+ // 이후의 폴백 제거·정직정지 강화로 죽은 길이 됐는데 메시지와 유령 작업만 남아
263
+ // "폴백한다"고 말하고 fail-closed로 죽는 모순을 냈다(실사용 storm 테스트 실증).
264
+ // 방침대로: 재시도 후에도 무효면 정직 정지.
265
+ for (let attempt = 0; attempt < 2 && !planned; attempt++) {
266
+ try {
267
+ const plannerText = await runBaseWorker(
268
+ [
269
+ coreHarnessPrompt,
270
+ workloadRouting.plannerSystemPrompt({
271
+ language: ui.lang === "ko" ? "Korean" : "English",
272
+ maxTasks: Math.min(SWARM_SPAWN_PER_TURN, SWARM_MAX_TASKS),
273
+ mode: stormbreaker ? "stormbreaker-goal-ultracode" : "swarm",
274
+ liveRuntimeInventory,
275
+ }),
276
+ attempt > 0
277
+ ? "PREVIOUS ATTEMPT WAS NOT VALID PLAN JSON. Reply with ONLY the plan JSON object — no prose, no tool use."
278
+ : "",
279
+ ].filter(Boolean).join("\n\n"),
280
+ ctx.routeContext
281
+ ? `${goal}\n\nHEPHAESTUS ROUTE EVIDENCE (advisory; the Agentlas parent owns the final plan):\n${ctx.routeContext}`
282
+ : goal,
283
+ { permission: "read", authorityMode: "no-authority" },
284
+ );
285
+ planned = workloadRouting.normalizePlan(plannerText, { maxTasks: SWARM_SPAWN_PER_TURN });
286
+ } catch (error) {
287
+ ui.warn(`workload planner failed: ${String((error && error.message) || error).slice(0, 160)}`);
288
+ }
289
+ }
290
+ ui.stopSpinner();
291
+ if (!planned) {
292
+ ui.error(ui.lang === "ko"
293
+ ? "플래너가 유효한 실행 계획(JSON)을 내지 못했습니다 — 정지합니다 (조용한 폴백 금지)."
294
+ : "The planner did not produce a valid plan JSON — stopping (no silent fallback).");
295
+ return { ok: false, reason: "invalid_plan_json" };
296
+ }
297
+ if (planned) {
298
+ ui.line("");
299
+ ui.info(stormbreaker
300
+ ? (ui.lang === "ko" ? "Stormbreaker Goal/UltraCode 실행 계획:" : "Stormbreaker Goal/UltraCode execution plan:")
301
+ : (ui.lang === "ko" ? "스웜 실행 계획:" : "Swarm execution plan:"));
302
+ for (const task of planned.tasks) {
303
+ const allocation = task.allocation;
304
+ ui.line(` ${ui.c.emerald("▸ ")}${ui.c.text(task.title)}${ui.c.dim(` ${allocation.runtimeId || "current"} · ${allocation.exactModelId || allocation.tier} · ${allocation.effort}`)}`);
305
+ }
306
+ ui.line(` ${ui.c.emerald("◆ ")}${ui.c.text("synthesis")}${ui.c.dim(` ${planned.synthesis.runtimeId || "current"} · ${planned.synthesis.exactModelId || planned.synthesis.tier} · ${planned.synthesis.effort}`)}`);
307
+ }
308
+
309
+ let seq = 0;
310
+ const initialTasks = planned.tasks; // 정직 정지 이후 planned는 항상 유효 — 유령 폴백 작업 경로 제거
311
+ const tasks = initialTasks.map((task) => ({ id: ++seq, ...task, status: "pending", result: "", parentTaskId: null }));
312
+ const seen = new Set(tasks.map((task) => task.title.toLowerCase()));
313
+ let active = 0;
314
+ let failed = 0;
315
+
316
+ await new Promise((resolveAll) => {
317
+ const pump = () => {
318
+ const pending = tasks.filter((t) => t.status === "pending");
319
+ if (!pending.length && active === 0) return resolveAll();
320
+ for (const task of pending) {
321
+ if (active >= concurrency) break;
322
+ task.status = "running";
323
+ active++;
324
+ ui.tool(`⚑ ${task.title}` + (task.role ? ` (${task.role})` : ""));
325
+ runAllocatedWorker(
326
+ [coreHarnessPrompt, swarmProtocol(goal, tasks, task, liveRuntimeInventory)].filter(Boolean).join("\n\n"),
327
+ task.brief || task.title,
328
+ task,
329
+ "worker",
330
+ task.parentTaskId,
331
+ )
332
+ .then((text) => {
333
+ const parsed = parseSwarmOutput(text);
334
+ task.status = "done";
335
+ task.result = parsed.result;
336
+ ui.toolResult(parsed.result.split("\n").slice(0, 3).join("\n") || "(empty result)", true);
337
+ for (const s of parsed.spawn) {
338
+ const key = s.title.toLowerCase();
339
+ if (tasks.length >= SWARM_MAX_TASKS || seen.has(key)) continue;
340
+ seen.add(key);
341
+ tasks.push({ id: ++seq, title: s.title, brief: s.brief, role: s.role, allocation: s.allocation, status: "pending", result: "", parentTaskId: `worker-${task.id}` });
342
+ ui.info(`+ spawn: ${s.title}`);
343
+ }
344
+ })
345
+ .catch((e) => {
346
+ task.status = "failed";
347
+ failed++;
348
+ ui.toolResult(String((e && e.message) || e).slice(0, 200), false);
349
+ })
350
+ .finally(() => {
351
+ active--;
352
+ setImmediate(pump);
353
+ });
354
+ }
355
+ };
356
+ pump();
357
+ });
358
+
359
+ const done = tasks.filter((t) => t.status === "done" && t.result);
360
+ ui.line("");
361
+ ui.info(`tasks: ${tasks.length} · done: ${done.length} · failed: ${failed}`);
362
+ if (!done.length) {
363
+ ui.error(ui.lang === "ko" ? "스웜이 완료한 작업이 없습니다." : "The swarm completed no work.");
364
+ return { ok: false };
365
+ }
366
+
367
+ ui.startSpinner(ui.lang === "ko" ? "스웜 결과 종합 중…" : "Synthesizing swarm results…");
368
+ // HOST-VERIFIED ALLOCATION: 최종 게이트(종합자)는 호스트가 검증한 런타임·모델·
369
+ // effort 증거를 그대로 받는다 — 워커 자기 신고가 아니라 resolve 결과다.
370
+ const pieces = done.map((t, i) => [
371
+ `### ${i + 1}. ${t.title}`,
372
+ `HOST-VERIFIED ALLOCATION: ${JSON.stringify(t.resolvedAllocation || null)}`,
373
+ t.result,
374
+ ].join("\n")).join("\n\n");
375
+ let finalText;
376
+ try {
377
+ const synthesisTask = {
378
+ id: "final",
379
+ title: "swarm synthesis",
380
+ brief: goal,
381
+ allocation: planned && planned.synthesis,
382
+ };
383
+ finalText = await runAllocatedWorker(
384
+ [
385
+ coreHarnessPrompt,
386
+ "You are the synthesizer of an agent swarm. Below are the results your peers produced for the shared goal.",
387
+ "Integrate them into ONE coherent final answer for the user. Reconcile overlaps, note anything incomplete.",
388
+ "Do not just concatenate. Do not include a `## Spawn` block.",
389
+ `SHARED GOAL: ${goal}`,
390
+ `Answer in the user's language (${ui.lang === "ko" ? "Korean" : "English"}).`,
391
+ ].join("\n"),
392
+ pieces,
393
+ synthesisTask,
394
+ "synthesis",
395
+ );
396
+ } catch (e) {
397
+ ui.stopSpinner();
398
+ ui.error("Synthesis failed: " + String((e && e.message) || e).slice(0, 200));
399
+ finalText = pieces;
400
+ }
401
+ ui.stopSpinner();
402
+ ui.line("");
403
+ ui.markdown(String(finalText).trim());
404
+ return { ok: true, finalText, taskCount: tasks.length, doneCount: done.length };
405
+ }
406
+
407
+ async function cmdSwarm(db, args, runtimeOverride, executionContext = {}) {
408
+ const rest = [];
409
+ let concurrency;
410
+ for (let i = 0; i < args.length; i++) {
411
+ if (args[i] === "--parallel" || args[i] === "-n") concurrency = Number(args[++i]);
412
+ else rest.push(args[i]);
413
+ }
414
+ const r = await swarmRun(db, rest.join(" "), { ...executionContext, concurrency, runtimeOverride });
415
+ if (!r.ok) process.exitCode = 1;
416
+ return r;
417
+ }
418
+
419
+ return { swarmRun, cmdSwarm, swarmProtocol, parseSwarmOutput };
420
+ }
421
+
422
+ module.exports = { create, swarmProtocol, parseSwarmOutput, SWARM_MAX_TASKS, SWARM_SPAWN_PER_TURN };