agentlas 0.7.0 → 0.9.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 (49) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +161 -18
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-core-harness.cjs +212 -0
  5. package/engine/agentlas-desktop-loadout.cjs +527 -0
  6. package/engine/agentlas-doctor.cjs +1 -1
  7. package/engine/agentlas-experience-exchange.cjs +835 -85
  8. package/engine/agentlas-experience-intake.cjs +444 -0
  9. package/engine/agentlas-experience-mcp.cjs +580 -18
  10. package/engine/agentlas-i18n.cjs +10 -10
  11. package/engine/agentlas-input.cjs +5 -4
  12. package/engine/agentlas-mcp-env.cjs +219 -0
  13. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  14. package/engine/agentlas-memory-governance.cjs +1029 -0
  15. package/engine/agentlas-native-host.cjs +129 -39
  16. package/engine/agentlas-parity.cjs +339 -154
  17. package/engine/agentlas-repl.cjs +306 -31
  18. package/engine/agentlas-workforce.cjs +2991 -0
  19. package/engine/agentlas-workload-routing.cjs +523 -0
  20. package/engine/agentlas.cjs +1619 -234
  21. package/engine/bootstrap-schema.sql +1 -1
  22. package/engine/experience-taxonomy-v1.json +49 -0
  23. package/package.json +8 -4
  24. package/scripts/gen-bootstrap-schema.sh +0 -23
  25. package/test/bootstrap-race.cjs +0 -47
  26. package/test/capture-runtime-guard.cjs +0 -122
  27. package/test/cloud-asset-restore.cjs +0 -423
  28. package/test/cloud-cas-client.cjs +0 -333
  29. package/test/cloud-owner-restore.cjs +0 -183
  30. package/test/cloud-runtime-paths.cjs +0 -40
  31. package/test/cloud-save-publish.cjs +0 -487
  32. package/test/credential-env-regression.cjs +0 -52
  33. package/test/engine-hardening-regression.cjs +0 -74
  34. package/test/experience-exchange-contract.cjs +0 -569
  35. package/test/experience-mcp-contract.cjs +0 -391
  36. package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
  37. package/test/login-loopback-security.cjs +0 -115
  38. package/test/mcp-config-isolation.cjs +0 -36
  39. package/test/permission-mapping.cjs +0 -180
  40. package/test/route-regression.cjs +0 -357
  41. package/test/run-api-regression.cjs +0 -322
  42. package/test/runtime-env-protection.cjs +0 -89
  43. package/test/semver-precedence.cjs +0 -39
  44. package/test/smoke.sh +0 -93
  45. package/test/sqlite-driver-probe.cjs +0 -22
  46. package/test/terminal-ui-regression.cjs +0 -477
  47. package/test/timeout-regression.cjs +0 -218
  48. package/test/tool-workspace-boundary.cjs +0 -165
  49. 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,40 +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);
329
404
  // claude(--resume)·codex(resume <thread_id>)만 서버측에서 대화를 이어받아 이전 턴의
330
405
  // 시스템 프롬프트를 유지한다(native-host.cjs claudeArgs/codexArgs). gemini CLI는 resume
331
406
  // 플래그가 없어 매 턴이 새 프로세스 — session.id로 시스템 프롬프트를 비우면 2턴째부터
332
407
  // 페르소나가 통째로 사라진다. 그래서 gemini는 매 턴 시스템 프롬프트를 다시 보낸다.
333
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
+ : [];
334
418
  const res = await runNativeTurn({
335
419
  kind: rt.kind,
336
420
  bin,
337
- prompt,
421
+ prompt: runtimePrompt,
338
422
  systemPrompt: session.id && resumesServerSide ? "" : sys,
339
423
  cwd: state.cwd,
340
424
  permission: state.permission,
341
425
  session,
342
- model: rt.model || null, // /model (claude --model, codex -m, gemini -m)
343
- effort: state.effort || null, // /effort (codex reasoning effort, claude think-keyword)
344
- mcpServers:
345
- state.permission === "full" && H.mcpServers
346
- ? H.mcpServers(db).filter((s) => s.enabled && s.transport === "stdio")
347
- : [],
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,
348
429
  env: runEnv,
349
- ui: assistantUi,
430
+ ui: memoryGuard,
350
431
  signal,
351
432
  });
352
- 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
+ }
353
456
  if (recordHistoryEntry && at && !res.error) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: at });
354
457
  recordCost(costLabel, res.usage);
355
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
+ }
356
478
  } else {
357
479
  const subjectSystem = state.routePreambleOnce
358
480
  ? `${state.routePreambleOnce}\n\n${state.subject.system}`
359
481
  : state.subject.system;
360
482
  state.routePreambleOnce = null;
361
- const sys = H.augmentSystem(db, subjectSystem, ctx, true);
483
+ const sys = H.augmentSystem(db, subjectSystem, ctx, true, prompt);
362
484
  let apiKey = null;
363
485
  if (rt.backend !== "ollama") {
364
486
  apiKey = await H.apiKey(rt.backend);
@@ -374,7 +496,7 @@ function startRepl(opts) {
374
496
  const guard = makeMemoryGuard(assistantUi, H.eventsHeading());
375
497
  const res = await runApiTurn({
376
498
  backend: rt.backend,
377
- model: rt.model || H.defaultApiModel(rt.backend),
499
+ model: selectedModel || H.defaultApiModel(rt.backend),
378
500
  apiKey,
379
501
  system: sys,
380
502
  messages,
@@ -382,12 +504,69 @@ function startRepl(opts) {
382
504
  ui: guard,
383
505
  signal,
384
506
  });
385
- 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
+ }
386
529
  if (recordHistoryEntry && cleaned) state.history.push({ role: "user", text: prompt }, { role: "assistant", text: cleaned });
387
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
+ }
388
549
  }
389
550
  } catch (e) {
390
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
+ }
391
570
  if (signal.aborted) {
392
571
  // user Ctrl-C — SIGINT handler already printed
393
572
  } else if (e && e.name === "AbortError") {
@@ -396,6 +575,21 @@ function startRepl(opts) {
396
575
  ui.error((e && e.message) || String(e));
397
576
  }
398
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
+ }
399
593
  busy = false;
400
594
  ui.endTurn();
401
595
  currentAbort = null;
@@ -412,6 +606,7 @@ function startRepl(opts) {
412
606
  const bin = H.which(H.RUNTIME_BIN[a]);
413
607
  if (!bin) return ui.error(ui.t("runtimeNotInstalled", a));
414
608
  state.runtime = { mode: "cli", kind: a };
609
+ state.modelPinned = false;
415
610
  baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정 (이후 auto-route가 덮어쓰지 않게)
416
611
  state.native = {};
417
612
  return ui.ok(ui.t("runtimeSet", a));
@@ -421,6 +616,7 @@ function startRepl(opts) {
421
616
  a === "ollama"
422
617
  ? { mode: "api", backend: "ollama", model: state.runtime.backend === "ollama" ? state.runtime.model : null }
423
618
  : { mode: "api", backend: a, model: null };
619
+ state.modelPinned = false;
424
620
  baseRuntime = state.runtime; // 명시적 /runtime 은 세션 기본으로 고정
425
621
  return ui.ok(ui.t("runtimeSet", runtimeLabel(state.runtime)));
426
622
  }
@@ -443,6 +639,7 @@ function startRepl(opts) {
443
639
  if (pinned && pinned !== "auto") spec = pinned;
444
640
  else spec = caps.autoRuntimeFor(subject.capAgent, { installedKinds: installedKinds(), activeSpec: caps.specOf(baseRuntime) });
445
641
  state.runtime = caps.runtimeFromSpec(spec);
642
+ state.modelPinned = false;
446
643
  state.native = {};
447
644
  }
448
645
  // Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
@@ -777,15 +974,29 @@ function startRepl(opts) {
777
974
  research = true;
778
975
  goal = goal.replace(/\s?--research(-evidence)?\b/g, "").trim();
779
976
  }
780
- 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
+ });
781
987
  return true;
782
988
  }
783
989
  case "build": {
784
990
  if (!arg) return ui.warn("usage: /build <만들고 싶은 에이전트/팀 설명>"), true;
785
991
  ui.line("");
786
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
+ }
787
996
  await H.terminalBuild(db, arg, {
788
997
  cwd: state.cwd,
998
+ modelPin: state.modelPinned ? state.runtime.model : null,
999
+ effortPin: state.effortPinned ? (state.effort || "none") : undefined,
789
1000
  input: process.stdin,
790
1001
  promptOutput: process.stderr,
791
1002
  out: (line) => ui.line(String(line)),
@@ -810,9 +1021,38 @@ function startRepl(opts) {
810
1021
  else await H.hepRun(["hep-search", arg], { cwd: state.cwd });
811
1022
  return true;
812
1023
  }
1024
+ case "workforce":
813
1025
  case "network":
814
1026
  case "taskforce": {
815
- 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;
816
1056
  ui.line("");
817
1057
  await H.hepRun(["hep-network", arg, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
818
1058
  return true;
@@ -836,6 +1076,9 @@ function startRepl(opts) {
836
1076
  concurrency = Number(m[1]);
837
1077
  goal = goal.replace(m[0], "").trim();
838
1078
  }
1079
+ if (H.ensureProjectForExecution) {
1080
+ state.projectPath = H.ensureProjectForExecution(db, state.cwd, state.permission, "terminal-swarm");
1081
+ }
839
1082
  await H.swarmRun(db, goal, {
840
1083
  ui,
841
1084
  cwd: state.cwd,
@@ -844,27 +1087,36 @@ function startRepl(opts) {
844
1087
  concurrency,
845
1088
  agent: state.subject && state.subject.capAgent,
846
1089
  projectPath: state.projectPath,
1090
+ modelPin: state.modelPinned ? state.runtime.model : null,
1091
+ effortPin: state.effortPinned ? (state.effort || "none") : undefined,
847
1092
  });
848
1093
  return true;
849
1094
  }
850
- case "model":
1095
+ case "model": {
851
1096
  // CLI(claude/codex/gemini)와 BYOK/Ollama 모두 지원 — 각 런타임의 모델 플래그로 전달.
852
- 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);
853
1100
  state.native = {}; // 새 모델로 세션 리셋
854
- ui.ok(ui.t("modelSet", state.runtime.model || ui.t("modelDefault")));
1101
+ ui.ok(ui.t("modelSet", state.runtime.model || "auto"));
855
1102
  return true;
1103
+ }
856
1104
  case "effort": {
857
1105
  const lv = (arg || "").toLowerCase().trim();
858
1106
  if (!lv) {
859
- ui.info(ui.t("effortCurrent", state.effort || ui.t("modelDefault")));
1107
+ ui.info(ui.t("effortCurrent", state.effortPinned ? (state.effort || "off") : "auto"));
860
1108
  return true;
861
1109
  }
862
1110
  if (!["low", "medium", "high", "max", "auto", "off"].includes(lv)) {
863
1111
  return ui.warn(ui.t("effortUsage")), true;
864
1112
  }
865
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);
866
1118
  state.native = {};
867
- ui.ok(ui.t("effortSet", state.effort || ui.t("modelDefault")));
1119
+ ui.ok(ui.t("effortSet", state.effortPinned ? (state.effort || "off") : "auto"));
868
1120
  return true;
869
1121
  }
870
1122
  case "permission":
@@ -985,16 +1237,15 @@ function startRepl(opts) {
985
1237
  const servers = H.mcpServers ? H.mcpServers(db) : [];
986
1238
  ui.line("");
987
1239
  ui.rule("MCP");
988
- 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")));
989
1240
  for (const s of servers) {
990
1241
  let envKeys = [];
991
1242
  try { envKeys = JSON.parse(s.env_keys_json || "[]"); } catch { /* ignore */ }
992
1243
  const name = ui.lang === "en" ? (s.name_en || s.name) : s.name;
993
- 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");
994
1245
  const envStr = envKeys.length ? envKeys.join(", ") : "no key";
995
1246
  ui.line(" " + ui.c.emerald(String(name).padEnd(22)) + ui.c.blue(String(s.transport || "").padEnd(7)) + on + ui.c.dim(" " + envStr));
996
1247
  }
997
- 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;
998
1249
  ui.line(" " + ui.c.faint(ui.t("mcp.wired", String(wired))));
999
1250
  ui.line(" " + ui.c.faint(ui.t("mcp.usage")));
1000
1251
  return true;
@@ -1221,17 +1472,29 @@ function startRepl(opts) {
1221
1472
  }
1222
1473
  }
1223
1474
  }
1224
- // 자동 엔진 개입은 명시적 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.
1225
1477
  if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
1226
1478
  if (prefs.autoStorm && H.stormRun) {
1227
1479
  ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
1228
1480
  await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
1229
1481
  return;
1230
1482
  }
1231
- if (prefs.autoNetwork && H.hepRun) {
1232
- 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
+ }
1233
1488
  ui.line("");
1234
- 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
+ });
1235
1498
  return;
1236
1499
  }
1237
1500
  }
@@ -1408,6 +1671,11 @@ function startRepl(opts) {
1408
1671
  bootstrap();
1409
1672
  }
1410
1673
 
1674
+ function applyPreferenceDefaults(prefs) {
1675
+ if (!Object.prototype.hasOwnProperty.call(prefs, "autoNetwork")) prefs.autoNetwork = true;
1676
+ return prefs;
1677
+ }
1678
+
1411
1679
  function printHelp(ui) {
1412
1680
  const c = ui.c;
1413
1681
  ui.line("");
@@ -1480,4 +1748,11 @@ function printKeybindings(ui) {
1480
1748
  for (const [k, v] of tips) ui.line(" " + c.emerald(k.padEnd(24)) + c.dim(v));
1481
1749
  }
1482
1750
 
1483
- module.exports = { startRepl, runtimeLabel, makeMemoryGuard, makeStyleGuard };
1751
+ module.exports = {
1752
+ startRepl,
1753
+ runtimeLabel,
1754
+ runtimePromptForSession,
1755
+ makeMemoryGuard,
1756
+ makeStyleGuard,
1757
+ applyPreferenceDefaults,
1758
+ };