agentlas 0.6.0 → 0.9.1

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 (46) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +220 -4
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-capabilities.cjs +34 -3
  5. package/engine/agentlas-core-harness.cjs +205 -0
  6. package/engine/agentlas-desktop-loadout.cjs +527 -0
  7. package/engine/agentlas-doctor.cjs +1 -1
  8. package/engine/agentlas-experience-exchange.cjs +2151 -0
  9. package/engine/agentlas-experience-intake.cjs +444 -0
  10. package/engine/agentlas-experience-mcp.cjs +1709 -0
  11. package/engine/agentlas-i18n.cjs +10 -10
  12. package/engine/agentlas-input.cjs +5 -4
  13. package/engine/agentlas-mcp-env.cjs +219 -0
  14. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  15. package/engine/agentlas-memory-governance.cjs +1029 -0
  16. package/engine/agentlas-native-host.cjs +129 -39
  17. package/engine/agentlas-parity.cjs +339 -154
  18. package/engine/agentlas-repl.cjs +327 -44
  19. package/engine/agentlas-workforce.cjs +2991 -0
  20. package/engine/agentlas-workload-routing.cjs +523 -0
  21. package/engine/agentlas.cjs +1886 -270
  22. package/engine/bootstrap-schema.sql +1 -1
  23. package/engine/experience-taxonomy-v1.json +49 -0
  24. package/package.json +8 -4
  25. package/scripts/gen-bootstrap-schema.sh +0 -23
  26. package/test/bootstrap-race.cjs +0 -47
  27. package/test/capture-runtime-guard.cjs +0 -122
  28. package/test/cloud-asset-restore.cjs +0 -423
  29. package/test/cloud-cas-client.cjs +0 -333
  30. package/test/cloud-owner-restore.cjs +0 -183
  31. package/test/cloud-runtime-paths.cjs +0 -40
  32. package/test/cloud-save-publish.cjs +0 -453
  33. package/test/credential-env-regression.cjs +0 -52
  34. package/test/login-loopback-security.cjs +0 -115
  35. package/test/mcp-config-isolation.cjs +0 -36
  36. package/test/permission-mapping.cjs +0 -180
  37. package/test/route-regression.cjs +0 -121
  38. package/test/run-api-regression.cjs +0 -322
  39. package/test/runtime-env-protection.cjs +0 -45
  40. package/test/semver-precedence.cjs +0 -39
  41. package/test/smoke.sh +0 -90
  42. package/test/sqlite-driver-probe.cjs +0 -22
  43. package/test/terminal-ui-regression.cjs +0 -472
  44. package/test/timeout-regression.cjs +0 -218
  45. package/test/tool-workspace-boundary.cjs +0 -165
  46. package/test/update-safety.cjs +0 -376
@@ -8,6 +8,7 @@
8
8
  * First launch runs an onboarding wizard (language → runtime → permission), stored in prefs.
9
9
  */
10
10
  const readline = require("node:readline");
11
+ const crypto = require("node:crypto");
11
12
  const { Ui } = require("./agentlas-ui.cjs");
12
13
  const banner = require("./agentlas-banner.cjs");
13
14
  const { runNativeTurn } = require("./agentlas-native-host.cjs");
@@ -24,6 +25,12 @@ function runtimeLabel(rt) {
24
25
  return `${rt.backend}${rt.model ? " · " + rt.model : ""}`;
25
26
  }
26
27
 
28
+ function runtimePromptForSession(prompt, ctx, rt, session, helpers) {
29
+ const resumesServerSide = rt && (rt.kind === "claude-code" || rt.kind === "codex");
30
+ if (!session?.id || !resumesServerSide || typeof helpers?.memoryEmitterPrompt !== "function") return prompt;
31
+ return `${prompt}\n\n${helpers.memoryEmitterPrompt(prompt, ctx)}`;
32
+ }
33
+
27
34
  function normalizeLang(value) {
28
35
  const v = String(value || "").toLowerCase();
29
36
  return v === "en" || v === "ko" ? v : null;
@@ -162,7 +169,7 @@ function makeStyleGuard(ui) {
162
169
  function startRepl(opts) {
163
170
  const { db } = opts;
164
171
  const H = opts.helpers;
165
- const prefs = opts.prefs || {};
172
+ const prefs = applyPreferenceDefaults(opts.prefs || {});
166
173
  prefs.agentRuntime = prefs.agentRuntime || {}; // { agentSlug|firmSlug: runtimeSpec|"auto" }
167
174
  let baseRuntime = opts.runtime; // session default; per-agent runtime auto-routes from this
168
175
  const ui = new Ui({ lang: terminalLang(prefs, opts) });
@@ -176,6 +183,8 @@ function startRepl(opts) {
176
183
  projectPath: opts.projectPath || null,
177
184
  routePreambleOnce: null,
178
185
  effort: prefs.effort || null, // /effort: low|medium|high|max → 런타임별 reasoning 강도
186
+ modelPinned: false, // true only after an explicit /model <id>; /model auto clears it
187
+ effortPinned: prefs.effortPinned === true || Boolean(prefs.effort), // /effort auto is the only automatic mode
179
188
  cost: {}, // runtimeLabel → { turns, in, out, cost, ms } — session usage ledger
180
189
  };
181
190
 
@@ -298,6 +307,16 @@ function startRepl(opts) {
298
307
  busy = true;
299
308
  currentAbort = new AbortController();
300
309
  const signal = currentAbort.signal;
310
+ let experienceRunId = `terminal-run:${crypto.randomUUID()}`;
311
+ const runStartedAt = Date.now();
312
+ let experienceFinalized = false;
313
+ let experienceContext = null;
314
+ let memorySettled = false;
315
+ let memoryContext = null;
316
+ let memoryRuntime = null;
317
+ let memoryModel = null;
318
+ let memoryOutput = "";
319
+ let memoryOutcome = "failed";
301
320
  try {
302
321
  ui.beginTurn({
303
322
  ...composerMeta(),
@@ -308,11 +327,67 @@ function startRepl(opts) {
308
327
  ui.warn(ui.t("interrupted"));
309
328
  },
310
329
  }); // 작업 중에도 composer/status bar와 실제 runtime task 목록을 화면 하단에 유지
330
+ if (H.ensureProjectForExecution) {
331
+ state.projectPath = H.ensureProjectForExecution(
332
+ db,
333
+ state.cwd,
334
+ state.permission,
335
+ "terminal-interactive-turn",
336
+ );
337
+ }
311
338
  const recordHistoryEntry = !runOptions.side;
312
339
  const targetLang = H.detectResponseLanguage ? H.detectResponseLanguage(prompt, ui.lang) : ui.lang;
313
- const ctx = { ...ctxNow(), lang: targetLang, uiLang: ui.lang };
340
+ const curatedMemories = [];
314
341
  const rt = state.runtime;
315
- const costLabel = runtimeLabel(rt);
342
+ const priorContextDigest = crypto.createHash("sha256")
343
+ .update(JSON.stringify(state.history.map((item) => ({ role: item.role, text: item.text }))))
344
+ .digest("hex");
345
+ const memoryTurn = typeof H.beginMemoryTurn === "function"
346
+ ? H.beginMemoryTurn(db, prompt, {
347
+ ...ctxNow(),
348
+ surface: runOptions.side ? "terminal-side-turn" : "terminal-interactive-turn",
349
+ conversationRef: rt.mode === "cli" ? state.native[rt.kind]?.id : null,
350
+ priorContextDigest,
351
+ // Provider session resume is conversation context, not turn
352
+ // identity. Every submitted host turn receives a fresh id.
353
+ stableTurnId: `terminal-turn:${crypto.randomUUID()}`,
354
+ })
355
+ : null;
356
+ if (memoryTurn?.turnId) experienceRunId = `terminal-run:${memoryTurn.turnId}`;
357
+ const ctx = {
358
+ ...ctxNow(),
359
+ lang: targetLang,
360
+ uiLang: ui.lang,
361
+ curatedMemories,
362
+ memoryTurn,
363
+ turnId: memoryTurn?.turnId || null,
364
+ };
365
+ memoryContext = ctx;
366
+ memoryRuntime = rt;
367
+ let workloadResolution = null;
368
+ if (state.subject && state.subject.kind === "firm" && typeof H.allocateWorkload === "function") {
369
+ ui.status(targetLang === "ko" ? "상위 AI가 팀 작업의 모델 비용을 배정 중…" : "Higher-level AI is allocating the team model…");
370
+ const planned = await H.allocateWorkload(db, prompt, {
371
+ runtime: rt,
372
+ cwd: state.cwd,
373
+ projectPath: state.projectPath,
374
+ agentId: state.subject.id,
375
+ lang: targetLang,
376
+ mode: "team",
377
+ modelPin: state.modelPinned ? rt.model : null,
378
+ effortPin: state.effortPinned ? (state.effort || "none") : undefined,
379
+ onWarning: (message) => ui.warn(message),
380
+ });
381
+ workloadResolution = planned && planned.resolution;
382
+ if (workloadResolution && workloadResolution.fallbackReason) {
383
+ ui.info(`model route: ${workloadResolution.source} · ${workloadResolution.model || runtimeLabel(rt)} · ${workloadResolution.fallbackReason}`);
384
+ }
385
+ }
386
+ const selectedModel = (workloadResolution && workloadResolution.model) || rt.model || null;
387
+ memoryModel = selectedModel;
388
+ const selectedEffort = workloadResolution ? workloadResolution.effort : state.effort || null;
389
+ const costLabel = selectedModel ? `${runtimeLabel(rt)} · ${selectedModel}` : runtimeLabel(rt);
390
+ experienceContext = { ctx, rt, selectedModel, curatedMemories, targetLang };
316
391
  const runEnv = H.buildChildEnv ? await H.buildChildEnv(db, { ...ctx, cwd: state.cwd }) : process.env;
317
392
  ui._lastUsage = null;
318
393
  const assistantUi = makeStyleGuard(ui);
@@ -325,35 +400,87 @@ function startRepl(opts) {
325
400
  ? `${state.routePreambleOnce}\n\n${state.subject.system}`
326
401
  : state.subject.system;
327
402
  state.routePreambleOnce = null;
328
- const sys = H.augmentSystem(db, subjectSystem, ctx, false);
403
+ const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
404
+ // claude(--resume)·codex(resume <thread_id>)만 서버측에서 대화를 이어받아 이전 턴의
405
+ // 시스템 프롬프트를 유지한다(native-host.cjs claudeArgs/codexArgs). gemini CLI는 resume
406
+ // 플래그가 없어 매 턴이 새 프로세스 — session.id로 시스템 프롬프트를 비우면 2턴째부터
407
+ // 페르소나가 통째로 사라진다. 그래서 gemini는 매 턴 시스템 프롬프트를 다시 보낸다.
408
+ const resumesServerSide = rt.kind === "claude-code" || rt.kind === "codex";
409
+ // Claude/Codex resume keeps the first turn's system prompt. Append the
410
+ // current host contract to this turn's user payload so a resumed model
411
+ // cannot reuse a stale Memory Ticket id from an earlier turn.
412
+ const runtimePrompt = runtimePromptForSession(prompt, ctx, rt, session, H);
413
+ const memoryGuard = makeMemoryGuard(assistantUi, H.eventsHeading());
414
+ const connectedMcpServers = state.permission === "full" && H.mcpServers
415
+ ? H.mcpServers(db).filter((server) =>
416
+ server.enabled && server.transport === "stdio" && server.runtimeEligible === true && server.runtimeConsented === true)
417
+ : [];
329
418
  const res = await runNativeTurn({
330
419
  kind: rt.kind,
331
420
  bin,
332
- prompt,
333
- systemPrompt: session.id ? "" : sys,
421
+ prompt: runtimePrompt,
422
+ systemPrompt: session.id && resumesServerSide ? "" : sys,
334
423
  cwd: state.cwd,
335
424
  permission: state.permission,
336
425
  session,
337
- model: rt.model || null, // /model (claude --model, codex -m, gemini -m)
338
- effort: state.effort || null, // /effort (codex reasoning effort, claude think-keyword)
339
- mcpServers:
340
- state.permission === "full" && H.mcpServers
341
- ? H.mcpServers(db).filter((s) => s.enabled && s.transport === "stdio")
342
- : [],
426
+ model: selectedModel, // explicit /model pin or higher-level AI allocation
427
+ effort: selectedEffort, // explicit /effort pin or higher-level AI allocation
428
+ mcpServers: connectedMcpServers,
343
429
  env: runEnv,
344
- ui: assistantUi,
430
+ ui: memoryGuard,
345
431
  signal,
346
432
  });
347
- const at = (res.text || "").trim();
433
+ memoryOutput = String(res.text || "");
434
+ memoryOutcome = res.error ? "failed" : "succeeded";
435
+ let at = "";
436
+ let memoryResult = null;
437
+ if (typeof H.completeMemoryTurn === "function") {
438
+ try {
439
+ memoryResult = await H.completeMemoryTurn(db, memoryOutput, ctx, rt, {
440
+ model: selectedModel,
441
+ outcome: memoryOutcome,
442
+ requestText: prompt,
443
+ invokeCurator: !res.error,
444
+ });
445
+ memorySettled = true;
446
+ } catch {
447
+ // finally retries once without semantic Curator authority.
448
+ }
449
+ for (const memory of memoryResult?.curatedMemories || []) {
450
+ if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
451
+ }
452
+ at = (H.sanitizeAssistantText(memoryResult?.cleaned ?? memoryOutput) || "").trim();
453
+ } else {
454
+ at = (H.sanitizeAssistantText(res.text || "") || "").trim();
455
+ }
348
456
  if (recordHistoryEntry && at && !res.error) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: at });
349
457
  recordCost(costLabel, res.usage);
350
458
  if (!runOptions.side && session.id && !res.error) persistSession(rt.kind, session.id, prompt);
459
+ if (typeof H.finalizeExperienceRun === "function") {
460
+ H.finalizeExperienceRun(db, {
461
+ agentId: ctx.agentId,
462
+ projectPath: ctx.projectPath,
463
+ cwd: state.cwd,
464
+ runtime: rt,
465
+ permission: state.permission,
466
+ model: selectedModel,
467
+ mcpServers: connectedMcpServers,
468
+ curatedMemories,
469
+ taskHint: prompt,
470
+ outcome: { status: res.error ? "failed" : "succeeded", failureCode: res.error ? "runtime-error" : null },
471
+ usage: res.usage,
472
+ durationMs: Date.now() - runStartedAt,
473
+ runId: experienceRunId,
474
+ lang: targetLang,
475
+ });
476
+ experienceFinalized = true;
477
+ }
351
478
  } else {
352
479
  const subjectSystem = state.routePreambleOnce
353
480
  ? `${state.routePreambleOnce}\n\n${state.subject.system}`
354
481
  : state.subject.system;
355
482
  state.routePreambleOnce = null;
356
- const sys = H.augmentSystem(db, subjectSystem, ctx, true);
483
+ const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
357
484
  let apiKey = null;
358
485
  if (rt.backend !== "ollama") {
359
486
  apiKey = await H.apiKey(rt.backend);
@@ -369,7 +496,7 @@ function startRepl(opts) {
369
496
  const guard = makeMemoryGuard(assistantUi, H.eventsHeading());
370
497
  const res = await runApiTurn({
371
498
  backend: rt.backend,
372
- model: rt.model || H.defaultApiModel(rt.backend),
499
+ model: selectedModel || H.defaultApiModel(rt.backend),
373
500
  apiKey,
374
501
  system: sys,
375
502
  messages,
@@ -377,12 +504,69 @@ function startRepl(opts) {
377
504
  ui: guard,
378
505
  signal,
379
506
  });
380
- const cleaned = (H.curateCliReply(db, res.text || "", ctx) || "").trim();
507
+ memoryOutput = String(res.text || "");
508
+ memoryOutcome = "succeeded";
509
+ let cleaned = "";
510
+ if (typeof H.completeMemoryTurn === "function") {
511
+ let memoryResult = null;
512
+ try {
513
+ memoryResult = await H.completeMemoryTurn(db, memoryOutput, ctx, rt, {
514
+ model: selectedModel,
515
+ outcome: memoryOutcome,
516
+ requestText: prompt,
517
+ });
518
+ memorySettled = true;
519
+ } catch {
520
+ // finally retries once without semantic Curator authority.
521
+ }
522
+ for (const memory of memoryResult?.curatedMemories || []) {
523
+ if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
524
+ }
525
+ cleaned = (H.sanitizeAssistantText(memoryResult?.cleaned ?? memoryOutput) || "").trim();
526
+ } else {
527
+ cleaned = (H.sanitizeAssistantText(res.text || "") || "").trim();
528
+ }
381
529
  if (recordHistoryEntry && cleaned) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: cleaned });
382
530
  recordCost(costLabel, ui._lastUsage);
531
+ if (typeof H.finalizeExperienceRun === "function") {
532
+ H.finalizeExperienceRun(db, {
533
+ agentId: ctx.agentId,
534
+ projectPath: ctx.projectPath,
535
+ cwd: state.cwd,
536
+ runtime: rt,
537
+ permission: state.permission,
538
+ model: selectedModel,
539
+ curatedMemories,
540
+ taskHint: prompt,
541
+ outcome: { status: "succeeded", failureCode: null },
542
+ usage: ui._lastUsage,
543
+ durationMs: Date.now() - runStartedAt,
544
+ runId: experienceRunId,
545
+ lang: targetLang,
546
+ });
547
+ experienceFinalized = true;
548
+ }
383
549
  }
384
550
  } catch (e) {
385
551
  ui.stopSpinner();
552
+ memoryOutcome = signal.aborted ? "cancelled" : "failed";
553
+ if (!experienceFinalized && experienceContext && typeof H.finalizeExperienceRun === "function") {
554
+ H.finalizeExperienceRun(db, {
555
+ agentId: experienceContext.ctx.agentId,
556
+ projectPath: experienceContext.ctx.projectPath,
557
+ cwd: state.cwd,
558
+ runtime: experienceContext.rt,
559
+ permission: experienceContext.ctx.permission,
560
+ model: experienceContext.selectedModel,
561
+ curatedMemories: experienceContext.curatedMemories,
562
+ taskHint: prompt,
563
+ outcome: { status: signal.aborted ? "cancelled" : "failed", failureCode: signal.aborted ? "operator-cancelled" : "runtime-error" },
564
+ durationMs: Date.now() - runStartedAt,
565
+ runId: experienceRunId,
566
+ lang: experienceContext.targetLang,
567
+ });
568
+ experienceFinalized = true;
569
+ }
386
570
  if (signal.aborted) {
387
571
  // user Ctrl-C — SIGINT handler already printed
388
572
  } else if (e && e.name === "AbortError") {
@@ -391,6 +575,21 @@ function startRepl(opts) {
391
575
  ui.error((e && e.message) || String(e));
392
576
  }
393
577
  } finally {
578
+ if (!memorySettled && memoryContext?.turnId && typeof H.completeMemoryTurn === "function") {
579
+ try {
580
+ await H.completeMemoryTurn(db, memoryOutput, memoryContext, memoryRuntime, {
581
+ model: memoryModel,
582
+ outcome: memoryOutcome,
583
+ requestText: prompt,
584
+ invokeCurator: false,
585
+ });
586
+ memorySettled = true;
587
+ } catch {
588
+ ui.warn(memoryContext.lang === "ko"
589
+ ? "메모리 영수증을 저장하지 못했습니다. 응답 실행은 유지됩니다."
590
+ : "The memory receipt could not be persisted; the response run is preserved.");
591
+ }
592
+ }
394
593
  busy = false;
395
594
  ui.endTurn();
396
595
  currentAbort = null;
@@ -407,6 +606,7 @@ function startRepl(opts) {
407
606
  const bin = H.which(H.RUNTIME_BIN[a]);
408
607
  if (!bin) return ui.error(ui.t("runtimeNotInstalled", a));
409
608
  state.runtime = { mode: "cli", kind: a };
609
+ state.modelPinned = false;
410
610
  baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정 (이후 auto-route가 덮어쓰지 않게)
411
611
  state.native = {};
412
612
  return ui.ok(ui.t("runtimeSet", a));
@@ -416,6 +616,7 @@ function startRepl(opts) {
416
616
  a === "ollama"
417
617
  ? { mode: "api", backend: "ollama", model: state.runtime.backend === "ollama" ? state.runtime.model : null }
418
618
  : { mode: "api", backend: a, model: null };
619
+ state.modelPinned = false;
419
620
  baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정
420
621
  return ui.ok(ui.t("runtimeSet", runtimeLabel(state.runtime)));
421
622
  }
@@ -438,6 +639,7 @@ function startRepl(opts) {
438
639
  if (pinned && pinned !== "auto") spec = pinned;
439
640
  else spec = caps.autoRuntimeFor(subject.capAgent, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
440
641
  state.runtime = caps.runtimeFromSpec(spec);
642
+ state.modelPinned = false;
441
643
  state.native = {};
442
644
  }
443
645
  // Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
@@ -492,7 +694,7 @@ function startRepl(opts) {
492
694
  slug: firm.slug,
493
695
  label: displayName(firm) + " CEO",
494
696
  system: sys,
495
- capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
697
+ capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, tagline_en: firm.tagline_en, entity_kind: "team", system_prompt: sys },
496
698
  };
497
699
  state.history = [];
498
700
  state.routePreambleOnce = null;
@@ -772,13 +974,33 @@ function startRepl(opts) {
772
974
  research = true;
773
975
  goal = goal.replace(/\s?--research(-evidence)?\b/g, "").trim();
774
976
  }
775
- await H.stormRun(db, goal, { ui, cwd: state.cwd, research });
977
+ if (H.ensureProjectForExecution) {
978
+ state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-storm");
979
+ }
980
+ await H.stormRun(db, goal, {
981
+ ui,
982
+ cwd: state.cwd,
983
+ permission: state.permission,
984
+ projectPath: state.projectPath,
985
+ research,
986
+ });
776
987
  return true;
777
988
  }
778
989
  case "build": {
779
990
  if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
780
991
  ui.line("");
781
- await H.hepRun(["hep-build", arg], { cwd: state.cwd });
992
+ if (typeof H.terminalBuild !== "function") throw new Error("Terminal MCP build preflight is unavailable");
993
+ if (H.ensureProjectForExecution) {
994
+ state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-build");
995
+ }
996
+ await H.terminalBuild(db, arg, {
997
+ cwd: state.cwd,
998
+ modelPin: state.modelPinned ? state.runtime.model : null,
999
+ effortPin: state.effortPinned ? (state.effort || "none") : undefined,
1000
+ input: process.stdin,
1001
+ promptOutput: process.stderr,
1002
+ out: (line) => ui.line(String(line)),
1003
+ });
782
1004
  return true;
783
1005
  }
784
1006
  case "route": {
@@ -799,17 +1021,38 @@ function startRepl(opts) {
799
1021
  else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
800
1022
  return true;
801
1023
  }
802
- case "install": {
803
- if (!arg) return ui.warn("usage: /install <slug> (/search 로 먼저 찾으세요)"), true;
804
- if (H.cloudInstall) {
805
- try { const r = await H.cloudInstall(db, arg.trim()); ui.ok((r && r.name) ? `설치됨: ${r.name}` : `설치됨: ${arg.trim()}`); }
806
- catch (e) { ui.error((e && e.message) || String(e)); }
807
- } else ui.warn("설치 기능을 사용할 수 없습니다.");
808
- return true;
809
- }
1024
+ case "workforce":
810
1025
  case "network":
811
1026
  case "taskforce": {
812
- if (!arg) return ui.warn("usage: /network <요청> (A2A 태스크포스)"), true;
1027
+ if (!arg) return ui.warn("usage: /network <요청> [--benchmark] (Agent Workforce Ontology)"), true;
1028
+ let goal = arg;
1029
+ const benchmark = /(?:^|\s)--benchmark(?:\s|$)/.test(goal);
1030
+ goal = goal.replace(/(?:^|\s)--benchmark(?=\s|$)/g, " ").trim();
1031
+ let concurrency;
1032
+ const parallel = goal.match(/(?:^|\s)--parallel\s+(\d+)\b/);
1033
+ if (parallel) {
1034
+ concurrency = Number(parallel[1]);
1035
+ goal = goal.replace(parallel[0], "").trim();
1036
+ }
1037
+ if (H.ensureProjectForExecution) {
1038
+ state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-workforce");
1039
+ }
1040
+ ui.line("");
1041
+ await H.workforceRun(db, goal, {
1042
+ ui,
1043
+ cwd: state.cwd,
1044
+ permission: state.permission,
1045
+ runtime: state.runtime,
1046
+ projectPath: state.projectPath,
1047
+ modelPin: state.modelPinned ? state.runtime.model : null,
1048
+ effortPin: state.effortPinned ? (state.effort || "none") : undefined,
1049
+ concurrency,
1050
+ benchmark,
1051
+ });
1052
+ return true;
1053
+ }
1054
+ case "legacy-network": {
1055
+ if (!arg) return ui.warn("usage: /legacy-network <요청>"), true;
813
1056
  ui.line("");
814
1057
  await H.hepRun(["hep-network", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
815
1058
  return true;
@@ -833,6 +1076,9 @@ function startRepl(opts) {
833
1076
  concurrency = Number(m[1]);
834
1077
  goal = goal.replace(m[0], "").trim();
835
1078
  }
1079
+ if (H.ensureProjectForExecution) {
1080
+ state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-swarm");
1081
+ }
836
1082
  await H.swarmRun(db, goal, {
837
1083
  ui,
838
1084
  cwd: state.cwd,
@@ -841,27 +1087,36 @@ function startRepl(opts) {
841
1087
  concurrency,
842
1088
  agent: state.subject && state.subject.capAgent,
843
1089
  projectPath: state.projectPath,
1090
+ modelPin: state.modelPinned ? state.runtime.model : null,
1091
+ effortPin: state.effortPinned ? (state.effort || "none") : undefined,
844
1092
  });
845
1093
  return true;
846
1094
  }
847
- case "model":
1095
+ case "model": {
848
1096
  // CLI(claude/codex/gemini)와 BYOK/Ollama 모두 지원 — 각 런타임의 모델 플래그로 전달.
849
- state.runtime.model = arg || null;
1097
+ const requested = (arg || "").trim();
1098
+ state.runtime.model = !requested || requested.toLowerCase() === "auto" ? null : requested;
1099
+ state.modelPinned = Boolean(state.runtime.model);
850
1100
  state.native = {}; // 새 모델로 세션 리셋
851
- ui.ok(ui.t("modelSet", state.runtime.model || ui.t("modelDefault")));
1101
+ ui.ok(ui.t("modelSet", state.runtime.model || "auto"));
852
1102
  return true;
1103
+ }
853
1104
  case "effort": {
854
1105
  const lv = (arg || "").toLowerCase().trim();
855
1106
  if (!lv) {
856
- ui.info(ui.t("effortCurrent", state.effort || ui.t("modelDefault")));
1107
+ ui.info(ui.t("effortCurrent", state.effortPinned ? (state.effort || "off") : "auto"));
857
1108
  return true;
858
1109
  }
859
1110
  if (!["low", "medium", "high", "max", "auto", "off"].includes(lv)) {
860
1111
  return ui.warn(ui.t("effortUsage")), true;
861
1112
  }
862
1113
  state.effort = lv === "auto" || lv === "off" ? null : lv;
1114
+ state.effortPinned = lv !== "auto";
1115
+ prefs.effort = state.effort;
1116
+ prefs.effortPinned = state.effortPinned;
1117
+ if (opts.savePrefs) opts.savePrefs(prefs);
863
1118
  state.native = {};
864
- ui.ok(ui.t("effortSet", state.effort || ui.t("modelDefault")));
1119
+ ui.ok(ui.t("effortSet", state.effortPinned ? (state.effort || "off") : "auto"));
865
1120
  return true;
866
1121
  }
867
1122
  case "permission":
@@ -982,16 +1237,15 @@ function startRepl(opts) {
982
1237
  const servers = H.mcpServers ? H.mcpServers(db) : [];
983
1238
  ui.line("");
984
1239
  ui.rule("MCP");
985
- ui.line(" " + ui.c.emerald(ui.t("mcp.playwright").padEnd(22)) + ui.c.blue("stdio ") + ui.c.green("on ") + ui.c.dim(" " + ui.t("mcp.fullOnly")));
986
1240
  for (const s of servers) {
987
1241
  let envKeys = [];
988
1242
  try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
989
1243
  const name = ui.lang === "en" ? (s.name_en || s.name) : s.name;
990
- const on = s.enabled ? ui.c.green("on ") : ui.c.faint("off");
1244
+ const on = s.enabled && s.runtimeConsented ? ui.c.green("on ") : s.enabled ? ui.c.faint("consent") : ui.c.faint("off");
991
1245
  const envStr = envKeys.length ? envKeys.join(", ") : "no key";
992
1246
  ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
993
1247
  }
994
- const wired = servers.filter((s) => s.enabled && s.transport === "stdio").length + 1; // +1 = full-only Playwright
1248
+ const wired = servers.filter((s) => s.enabled && s.transport === "stdio" && s.runtimeEligible === true && s.runtimeConsented === true).length;
995
1249
  ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
996
1250
  ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
997
1251
  return true;
@@ -1026,11 +1280,16 @@ function startRepl(opts) {
1026
1280
  } else {
1027
1281
  return ui.error(ui.t("noAgent", s.agentSlug)), true;
1028
1282
  }
1029
- if (s.kind && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
1283
+ // 저장된 CLI 런타임이 실제로 설치돼 있을 때만 그 세션을 복원한다. s.kind 없거나
1284
+ // (손상된 resume 레코드) 런타임이 미설치면, 잘못된/"undefined" 키로 native 맵을
1285
+ // 덮어써 현재 세션을 잃는 것을 막고 새 대화로 시작한다.
1286
+ if (s.kind && s.sessionId && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
1030
1287
  state.runtime = { mode: "cli", kind: s.kind };
1031
1288
  baseRuntime = state.runtime;
1289
+ state.native = { [s.kind]: { id: s.sessionId } };
1290
+ } else {
1291
+ state.native = {};
1032
1292
  }
1033
- state.native = { [s.kind]: { id: s.sessionId } };
1034
1293
  try {
1035
1294
  const fs2 = require("node:fs");
1036
1295
  if (s.cwd && fs2.existsSync(s.cwd)) state.cwd = s.cwd;
@@ -1213,17 +1472,29 @@ function startRepl(opts) {
1213
1472
  }
1214
1473
  }
1215
1474
  }
1216
- // 자동 엔진 개입은 명시적 opt-in 전용 (/config, 기본 off): direct 판정 + 실작업형 프롬프트일 때만.
1475
+ // Workforce is the default for direct, goal-like work. An explicit
1476
+ // `/config network off` remains a durable opt-out; Storm stays opt-in.
1217
1477
  if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
1218
1478
  if (prefs.autoStorm && H.stormRun) {
1219
1479
  ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
1220
1480
  await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
1221
1481
  return;
1222
1482
  }
1223
- if (prefs.autoNetwork && H.hepRun) {
1224
- ui.info(ui.t("config.autoEngage", "hep-network", "network"));
1483
+ if (prefs.autoNetwork && H.workforceRun) {
1484
+ ui.info(ui.t("config.autoEngage", "workforce ontology", "network"));
1485
+ if (H.ensureProjectForExecution) {
1486
+ state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-workforce-auto");
1487
+ }
1225
1488
  ui.line("");
1226
- await H.hepRun(["hep-network", t, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
1489
+ await H.workforceRun(db, t, {
1490
+ ui,
1491
+ cwd: state.cwd,
1492
+ permission: state.permission,
1493
+ runtime: state.runtime,
1494
+ projectPath: state.projectPath,
1495
+ modelPin: state.modelPinned ? state.runtime.model : null,
1496
+ effortPin: state.effortPinned ? (state.effort || "none") : undefined,
1497
+ });
1227
1498
  return;
1228
1499
  }
1229
1500
  }
@@ -1400,6 +1671,11 @@ function startRepl(opts) {
1400
1671
  bootstrap();
1401
1672
  }
1402
1673
 
1674
+ function applyPreferenceDefaults(prefs) {
1675
+ if (!Object.prototype.hasOwnProperty.call(prefs, "autoNetwork")) prefs.autoNetwork = true;
1676
+ return prefs;
1677
+ }
1678
+
1403
1679
  function printHelp(ui) {
1404
1680
  const c = ui.c;
1405
1681
  ui.line("");
@@ -1472,4 +1748,11 @@ function printKeybindings(ui) {
1472
1748
  for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
1473
1749
  }
1474
1750
 
1475
- module.exports = { startRepl, runtimeLabel, makeMemoryGuard, makeStyleGuard };
1751
+ module.exports = {
1752
+ startRepl,
1753
+ runtimeLabel,
1754
+ runtimePromptForSession,
1755
+ makeMemoryGuard,
1756
+ makeStyleGuard,
1757
+ applyPreferenceDefaults,
1758
+ };