agentlas 1.0.37 → 1.0.39

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.39 — 2026-08-11
4
+
5
+ pi-tui shell increment 2 + architecture mirror sync.
6
+
7
+ - Experimental pi shell (`AGENTLAS_TUI=pi`): persisted history (cli-history.json
8
+ v2 contract), Shift-Tab permission cycling (same two-step FULL arming state
9
+ machine as the classic REPL), `!` shell passthrough (full-permission gate,
10
+ secret masking), and `/s` `/switch` `/kill` `/rm` `/sessions` `/tree` session
11
+ observation wired to the same orchestrator/renderer pair.
12
+ - Streaming display masks the Memory Events envelope (runtime contract, not
13
+ user-facing text); the harvest pipeline is unchanged.
14
+ - ARCHITECTURE_VERSION mirror synced 1.7.0 → 1.7.1 (vendor regenerated from
15
+ the desktop dist, value-level parity gate green).
16
+
17
+ ## 1.0.38 — 2026-08-11
18
+
19
+ Silence was the worst failure mode — this release makes failures speak.
20
+
21
+ - The presentation boundary (`Ui.error`) now has conditions. Machine-coded
22
+ messages (usage guidance, honest stops, server relays) pass through; only
23
+ uncoded raw provider text is still replaced by the neutral recovery line.
24
+ Previously every failure — including usage help for eight argless slash
25
+ commands — collapsed into the same "One is recovering" sentence.
26
+ - `/s` `/switch` `/kill` `/rm` `/runtime` `/model` `/effort` `/permission`
27
+ without arguments now print their usage line instead of a recovery notice.
28
+ - Workforce sign-in expiry is relayed honestly: the server's `auth_required`
29
+ guidance reaches the user (run `agentlas login`), instead of being
30
+ misreported as an invalid continuity receipt and then swallowed.
31
+ - `doctor` verifies the cloud session against the server instead of only
32
+ checking that a session file exists. Expired sessions are reported as a
33
+ warning with the login hint; offline is reported as "unverified", never as
34
+ a false all-clear. `--json` output remains observation-only.
35
+ - Authored guidance in storm/swarm/workforce flows (planner refusals, unknown
36
+ options, persisted receipt issues) is no longer swallowed by the boundary.
37
+ - `npm run sync:architecture` works again — the script was restored to
38
+ `scripts/` where its relative paths are correct.
39
+
3
40
  ## 1.0.37 — 2026-08-10
4
41
 
5
42
  Agentlas One can now carry its owner-bound identity and curated memory tickets
@@ -5,7 +5,7 @@
5
5
  const path = require("node:path");
6
6
  const os = require("node:os");
7
7
  const permissions = require("./agentlas-permissions.cjs");
8
- const { truncateWidth, visWidth } = require("./agentlas-composer.cjs");
8
+ const { truncateWidth, visWidth } = require("./ui/width.cjs");
9
9
 
10
10
  // AGENTLAS wordmark (block letters). Rendered with a brand gradient across columns.
11
11
  const WORDMARK = [
@@ -5,7 +5,7 @@
5
5
  */
6
6
  const i18n = require("./agentlas-i18n.cjs");
7
7
  const banner = require("./agentlas-banner.cjs");
8
- const { visWidth, wrapWidth } = require("./agentlas-composer.cjs");
8
+ const { visWidth, wrapWidth } = require("./ui/width.cjs");
9
9
 
10
10
  /*
11
11
  * req = { ui, rl, helpers, persist } → Promise<{ onboarded, saved, saveError, lang, runtime, permission }>
@@ -184,7 +184,7 @@ async function runOnboard({ ui, rl, helpers, persist }) {
184
184
  }
185
185
  ui.line("");
186
186
  if (saveError) {
187
- ui.error(ui.t("wiz.saveFailed", String((saveError && saveError.message) || saveError)));
187
+ ui.error(ui.t("wiz.saveFailed", String((saveError && saveError.message) || saveError)), { reveal: true });
188
188
  } else {
189
189
  printSaved(ui.t("wiz.saved"));
190
190
  printIndented(ui.t("wiz.changeLang"), c.faint);
@@ -574,7 +574,7 @@ class Ui {
574
574
  this.updateSpinner(msg);
575
575
  }
576
576
  _message(prefix, prefixPaint, messagePaint, msg) {
577
- const { wrapWidth } = require("./agentlas-composer.cjs");
577
+ const { wrapWidth } = require("./ui/width.cjs");
578
578
  const columns = Math.max(8, Number(this.out.columns) || 80);
579
579
  const prefixWidth = visibleWidth(prefix);
580
580
  const lines = wrapWidth(stripAnsi(String(msg ?? "")), Math.max(2, columns - prefixWidth));
@@ -594,13 +594,28 @@ class Ui {
594
594
  this.stopSpinner();
595
595
  this._message("! ", this.c.amber, this.c.text, msg);
596
596
  }
597
- error(msg) {
597
+ error(msg, opts = {}) {
598
598
  this.stopSpinner();
599
- // Last-resort presentation boundary for legacy/direct commands. Raw
600
- // provider text, stack messages, paths and codes must never become UI.
601
- // REPL/session paths route the private evidence to the controller before
602
- // reaching this boundary; direct commands get a neutral recovery state.
603
- void msg;
599
+ /*
600
+ * 표시 경계 조건이 있어야 경계다 (2026-08-11, 존폐 판단 규칙 2).
601
+ * 무조건 삼키면 기계 코드·사용법·안내문까지 전부 같은 복구 줄이 되어
602
+ * 진단 자체가 불가능해진다(실사고: 죽은 명령 0인데 "안 돌아간다"로 보임).
603
+ * 통과 조건 — 저자가 의도한 메시지라는 기계 신호가 있을 때만:
604
+ * · Error 이고 code 또는 honestStop 이 있다 (usage/정직정지/서버 중계)
605
+ * · 호출자가 opts.reveal === true 로 명시했다 (storm/swarm 안내문 등)
606
+ * 신호 없는 문자열/Error 는 provider 날것으로 간주해 기존 계약대로 삼킨다
607
+ * (recovery-presentation-contract 가 이 삼킴을 계속 잠근다).
608
+ */
609
+ const isErrorLike = msg instanceof Error || (msg !== null && typeof msg === "object" && "message" in msg);
610
+ const machineSignal = isErrorLike && Boolean(msg.code || msg.honestStop);
611
+ if (opts.reveal === true || machineSignal) {
612
+ const raw = String((isErrorLike ? msg.message : msg) ?? "").trim();
613
+ const text = redactCommandSecrets(stripAnsi(raw)).slice(0, 800);
614
+ if (text) {
615
+ this._message("✖ ", this.c.amber, this.c.text, text);
616
+ return;
617
+ }
618
+ }
604
619
  this._message(
605
620
  "◆ ",
606
621
  this.c.amber,
@@ -1978,7 +1978,13 @@ function buildPrompts(task, identity) {
1978
1978
  function create(deps = {}) {
1979
1979
  const D = deps;
1980
1980
 
1981
+ /*
1982
+ * pi-tui 이행 정지작업 (D3 Phase 1-2, 2026-08-11): ctx가 준 Ui가 있으면 그것을
1983
+ * 쓴다. 자체 생성 Ui는 렌더러 교체 시 구 코드가 stdout에 직접 써 프레임을
1984
+ * 찢는 병렬 경로였다. 생성은 주입이 없을 때의 폴백으로만 남긴다.
1985
+ */
1981
1986
  function newUi(lang) {
1987
+ if (D.uiInstance) return D.uiInstance;
1982
1988
  return new Ui({ lang: lang || (typeof D.prefsLang === "function" ? D.prefsLang() : "en") });
1983
1989
  }
1984
1990
 
@@ -4549,10 +4555,10 @@ const TRANSIENT_MODEL_ERROR_RE = /Connection closed mid-response|"terminal_reaso
4549
4555
  const detailText = otherDetails && (typeof otherDetails !== "object" || Object.keys(otherDetails).length)
4550
4556
  ? ` — ${JSON.stringify(otherDetails).slice(0, 1_200)}`
4551
4557
  : "";
4552
- ui.error(`${receipt.failure.code}: ${receipt.failure.message}${detailText}`);
4558
+ ui.error(`${receipt.failure.code}: ${receipt.failure.message}${detailText}`, { reveal: true });
4553
4559
  if (issues) {
4554
- for (const issue of issues.slice(0, 16)) ui.error(` - ${String(issue).slice(0, 400)}`);
4555
- if (issues.length > 16) ui.error(` … ${issues.length - 16} more issues in the persisted receipt`);
4560
+ for (const issue of issues.slice(0, 16)) ui.error(` - ${String(issue).slice(0, 400)}`, { reveal: true });
4561
+ if (issues.length > 16) ui.error(` … ${issues.length - 16} more issues in the persisted receipt`, { reveal: true });
4556
4562
  }
4557
4563
  // 실패한 실행이야말로 토큰이 어디로 갔는지 알아야 하는 순간이다. issues 유무와
4558
4564
  // 무관하게 낸다 — 첫 배선이 이 블록 안에 들어가는 바람에 issues 없는 실패에서는
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.7.0",
2
+ "version": "1.7.1",
3
3
  "emitterBlock": "## Memory (Agentlas curated memory)\n\nAt the end of EVERY completed normal reply, emit exactly one hidden Memory Events\nenvelope. The runtime removes it before display. This envelope is the per-turn receipt:\nalways include a compact safe turn_summary, and use an empty candidates array when\nnothing durable was learned. Do not skip the envelope.\n\nRules:\n- Never include secrets, credentials, API keys, raw logs, or full transcripts.\n- Real credential values may live only in local project .env/.env.local,\n ignored signing/ or credentials/ files, or a local keychain/vault. Memory\n Events may mention env names and local relative paths only.\n- For deploy, release, store, billing, auth, API, or cloud work, first read the\n project's .agentlas/local-credentials.map.json and the top\n \"Local Credential Index\" section of .agentlas/project-soul-memory.md\n before saying a credential is missing.\n- One candidate per durable item. Keep \"content\" to one or two sentences.\n- \"memory_kind\": fact | decision | preference | risk | procedure | hypothesis | evidence | deprecation | conflict\n- \"suggested_scope\": user_identity | team_memory | project (this folder) | agent_repo | session (temporary) | discard\n- Use user_identity for a stable operator preference or personal fact (their name, role, language, tone,\n how they want you to behave) — these must outlive any one project. The curator only files user_identity\n when you label it so with \"confidence\": \"high\"; it never promotes into that scope, so a preference emitted\n at lower confidence is demoted to a throwaway session note.\n- \"agent_team\" is accepted only as a legacy alias for team_memory.\n- Add \"request_context\" when it improves future recall: user_intent, trigger_terms,\n cwd_at_request, target_project, target_path, cross_context, outcome.\n- Never put the raw user prompt or transcript in request_context.\n- Suggest a scope; the separate Memory Curator decides the final destination.\n- turn_summary is one value-free sentence about the completed outcome. It is not the\n user prompt, a transcript, raw log, secret, or absolute local path.\n\nFormat (always emit, including an empty candidates array):\n\n## Memory Events\n```json\n{\n \"schema_version\": \"agentlas.memory-ticket.v1\",\n \"turn_summary\": \"Completed outcome in one safe sentence.\",\n \"candidates\": [\n {\n \"memory_kind\": \"decision\",\n \"content\": \"...\",\n \"suggested_scope\": \"project\",\n \"confidence\": \"high\",\n \"sensitivity\": \"internal\",\n \"evidence_refs\": [],\n \"request_context\": {\n \"user_intent\": \"...\",\n \"trigger_terms\": [\"...\"],\n \"cwd_at_request\": null,\n \"target_project\": null,\n \"target_path\": null,\n \"cross_context\": false,\n \"outcome\": \"...\"\n }\n }\n ]\n}\n```",
4
4
  "eventsHeading": "## Memory Events",
5
5
  "memoryDir": ".agentlas",
@@ -24,7 +24,7 @@ function roleDetail(selection, role, en) {
24
24
  ].filter(Boolean).join(" · ");
25
25
  }
26
26
 
27
- function run(ctx, args = []) {
27
+ async function run(ctx, args = []) {
28
28
  const en = ctx.lang === "en";
29
29
  // clig.dev: 스크립트 소비자를 위한 기계 계약. 사람용 줄과 같은 사실만 담는다.
30
30
  if (ctx.output?.format === "json" || args.includes("--json")) {
@@ -124,14 +124,43 @@ function run(ctx, args = []) {
124
124
  }
125
125
  } catch { /* db issue already reported */ }
126
126
 
127
- // 3) 로그인 상태 (세션 파일 관측만 — 네트워크 호출 없음)
127
+ /*
128
+ * 3) 로그인 상태 — 자격 존재 + 실제 세션 검증.
129
+ * 파일 존재만 보고 all clear를 내면 만료 세션에서 편성이 죽는데 doctor는
130
+ * 초록불이었다(2026-08-11 존폐 판단 결함 5 — 사용자가 가장 먼저 칠 진단이
131
+ * 거짓말을 했다). 검증 실패는 종류를 가른다: 만료=경고(로그인 안내),
132
+ * 네트워크 불가=검증불가 표기(오프라인이 doctor를 적색으로 만들면 안 된다).
133
+ * --json 경로는 기존대로 관측만 한다(스크립트 소비자를 네트워크에 묶지 않는다).
134
+ */
128
135
  const sessionFile = path.join(userDataDir(), "auth", "cli-session.v1.json");
129
- if (process.env.AGENTLAS_SESSION) {
130
- ok(en ? "cloud session" : "클라우드 세션", "AGENTLAS_SESSION env");
131
- } else if (fs.existsSync(sessionFile)) {
132
- ok(en ? "cloud session" : "클라우드 세션", sessionFile);
133
- } else {
136
+ const auth = require("../cloud/auth.cjs");
137
+ const sessionCookie = auth.cloudSessionCookie();
138
+ const credentialLabel = process.env.AGENTLAS_SESSION
139
+ ? "AGENTLAS_SESSION env"
140
+ : (fs.existsSync(sessionFile) ? sessionFile : null);
141
+ if (!sessionCookie || !credentialLabel) {
134
142
  ctx.out(` ${ctx.ui.dim("·")} ${en ? "cloud session" : "클라우드 세션"}${ctx.ui.dim(en ? " — not signed in (agentlas login)" : " — 로그인 안 됨 (agentlas login)")}`);
143
+ } else {
144
+ try {
145
+ const meta = await auth.fetchSessionMeta(sessionCookie);
146
+ const who = meta && (meta.email || meta?.user?.email || meta?.account?.email || null);
147
+ ok(
148
+ en ? "cloud session" : "클라우드 세션",
149
+ `${credentialLabel}${who ? ` · ${who}` : ""} · ${en ? "verified" : "검증됨"}`,
150
+ );
151
+ } catch (sessionError) {
152
+ const status = /returned (\d{3})/.exec(String(sessionError?.message || ""))?.[1];
153
+ if (status === "401" || status === "403") {
154
+ warn(
155
+ en ? "cloud session" : "클라우드 세션",
156
+ en ? "expired — run `agentlas login`" : "만료됨 — `agentlas login`으로 다시 로그인",
157
+ );
158
+ } else {
159
+ ctx.out(` ${ctx.ui.dim("·")} ${en ? "cloud session" : "클라우드 세션"}${ctx.ui.dim(en
160
+ ? ` — ${credentialLabel} · unverified (network unreachable)`
161
+ : ` — ${credentialLabel} · 검증 불가 (네트워크 연결 안 됨)`)}`);
162
+ }
163
+ }
135
164
  }
136
165
 
137
166
  ctx.out("");
@@ -75,7 +75,7 @@ async function dispatch(ctx, command, args) {
75
75
  permission,
76
76
  `terminal-${command}`,
77
77
  ) || cwd;
78
- const runtime = workforceRuntime({ lang: ctx.lang, out: ctx.out });
78
+ const runtime = workforceRuntime({ lang: ctx.lang, out: ctx.out, uiInstance: ctx.uiInstance });
79
79
  const result = await runtime.cmdWorkforce(db, rest, runtimeOverride, {
80
80
  cwd,
81
81
  projectPath,
@@ -110,7 +110,7 @@ const RULES = [
110
110
  "",
111
111
  "Return ONLY compact JSON, one of these two shapes:",
112
112
  ' {"ask":[{"id":"<stable-id>","question":"...","why":"...","choices":["...","..."]}]}',
113
- ` {"blueprint":{"schema":"${BLUEPRINT_SCHEMA}","name":"...","goal":"...","trigger":{...},"steps":[...],"branches":[...]}}`,
113
+ ` {"blueprint":{"schema":"${BLUEPRINT_SCHEMA}","name":"...","goal":"...","trigger":{...},"steps":[...],"branches":[...],"checks":[...]}}`,
114
114
  "",
115
115
  'trigger is either {"kind":"cron","schedule":"daily-08:00"} (24h, or a 5-field cron string)',
116
116
  'or {"kind":"input","label":"<what to ask the person>","varName":"<one word, a-z>"}.',
@@ -226,8 +226,13 @@ function buildInterviewPrompt(state, locale = "ko") {
226
226
  lines.push(
227
227
  "",
228
228
  "Your previous blueprint could NOT be built. Fix exactly these problems and return a",
229
- "corrected blueprint. Do not repeat the same mistake, and do not ask the person about it ",
230
- "these are format problems on your side, not missing information:",
229
+ "corrected blueprint. EVERY fix below is ADDITIVE: add the missing top-level checks[] entry",
230
+ "(each problem message gives you the exact entry to add). Keep EVERY step, the trigger, and",
231
+ "every produces/consumes exactly as they are — never delete, merge, or shrink a step to make",
232
+ "a problem disappear: that removes what the person asked for and just triggers a different",
233
+ "error. More steps and more checks is the right direction, never fewer. Do not repeat the",
234
+ "same mistake, and do not ask the person — these are format problems on your side,",
235
+ "not missing information:",
231
236
  );
232
237
  for (const a of attempts) for (const problem of a.problems) lines.push(` · ${problem}`);
233
238
  }
@@ -250,6 +255,39 @@ function triggerQuestion() {
250
255
  };
251
256
  }
252
257
 
258
+ /**
259
+ * ★출력값 검증 check를 **코드가 채운다**(데스크탑 graph-blueprint.ts autofillOutputChecks와 동일).
260
+ * 바깥으로 나가는 단계가 소비하는 '앞에서 만든 값'에 check가 없으면, 검증기가 아는 그대로
261
+ * 표준 check를 넣어 **완전한 그래프를 완성**한다. 모델에 되물어 진동시키지 않고, 단계를 깎거나
262
+ * 캔버스로 떠넘기지도 않는다. 사람은 저장 확인 화면에서 항목을 보고 고칠 수 있다.
263
+ */
264
+ function autofillOutputChecks(bp) {
265
+ if (!bp || !Array.isArray(bp.steps)) return bp;
266
+ const checks = Array.isArray(bp.checks) ? [...bp.checks] : [];
267
+ const checked = new Set(checks.map((c) => (c.subject || "").trim()).filter(Boolean));
268
+ bp.steps.forEach((step, index) => {
269
+ if (step.effect !== "mutation") return;
270
+ for (const value of Array.isArray(step.consumes) ? step.consumes : []) {
271
+ const name = String(value == null ? "" : value).trim();
272
+ if (!name || checked.has(name)) continue;
273
+ const madeAt = bp.steps.findIndex((s, i) => i < index && (s.produces || "").trim() === name);
274
+ if (madeAt < 0) continue;
275
+ checks.push({
276
+ afterStep: madeAt,
277
+ subject: name,
278
+ criteria: `${name}이(가) 비어있지 않고 요청대로 채워졌다`,
279
+ produces: `${name}_ok`,
280
+ items: [
281
+ { text: `${name}이(가) 실제 내용으로 채워졌다`, kind: "must" },
282
+ { text: "빈 값·자리표시자·지어낸 값이 아니다", kind: "mustNot" },
283
+ ],
284
+ });
285
+ checked.add(name);
286
+ }
287
+ });
288
+ return { ...bp, checks };
289
+ }
290
+
253
291
  /** 청사진이 그래프로 지어질 수 있는지. 모자란 곳은 기본값이 아니라 질문으로 돌려준다. */
254
292
  function validateBlueprint(bp, ctx = {}) {
255
293
  const problems = [];
@@ -431,12 +469,15 @@ function validateBlueprint(bp, ctx = {}) {
431
469
  for (const value of consumes) {
432
470
  const name = String(value == null ? "" : value).trim();
433
471
  if (!name || checkedSubjects.has(name)) continue;
434
- const madeByAStep = steps.some((s, i) => i < index && (s.produces || "").trim() === name);
435
- if (!madeByAStep) continue;
472
+ const madeAt = steps.findIndex((s, i) => i < index && (s.produces || "").trim() === name);
473
+ if (madeAt < 0) continue;
436
474
  push(
437
475
  `"${step.title || `${index + 1}번째 단계`}"는 바깥으로 나가는데, 그 앞에서 만든 `
438
- + `"${name}" 값이 쓸 만한지 확인하는 단계가 없습니다. `
439
- + `checks[]에 {"afterStep":<그 값을 만든 단계>,"subject":"${name}",…}를 넣어 주세요.`,
476
+ + `"${name}" 값이 쓸 만한지 확인하는 단계가 없습니다. 단계는 하나도 지우지 말고, `
477
+ + `top-level checks[]에 항목을 그대로 추가하세요: `
478
+ + `{"afterStep":${madeAt},"subject":"${name}","criteria":"${name}이(가) 비어있지 않고 요청대로 채워졌다",`
479
+ + `"produces":"${name}_ok","items":[{"text":"${name}이(가) 실제 내용으로 채워졌다","kind":"must"},`
480
+ + `{"text":"빈 값·자리표시자·지어낸 값이 아니다","kind":"mustNot"}]}`,
440
481
  );
441
482
  }
442
483
  });
@@ -924,7 +965,8 @@ function parseInterviewTurn(text, state) {
924
965
 
925
966
  const blueprint = parsed.blueprint;
926
967
  if (!blueprint || typeof blueprint !== "object") return unreadable(text);
927
- const normalized = { ...blueprint, schema: BLUEPRINT_SCHEMA };
968
+ // ★출력값 검증 check는 코드가 채운다 부탁받은 완전한 그래프를 완성한다(깎지도 떠넘기지도 않음).
969
+ const normalized = autofillOutputChecks({ ...blueprint, schema: BLUEPRINT_SCHEMA });
928
970
  const problems = validateBlueprint(normalized);
929
971
  if (problems.length === 0) {
930
972
  // 검증은 통과했다. 그런데 **앞 시도보다 작아졌으면** 문제를 지워서 고친 것이다 —
@@ -949,12 +991,14 @@ function parseInterviewTurn(text, state) {
949
991
  };
950
992
  }
951
993
 
952
- /** 모델이 스스로 고쳐 볼 기회의 상한. 데스크탑과 같은 값. */
953
- const MAX_SELF_CORRECTIONS = 2;
994
+ /** 모델이 스스로 고쳐 볼 기회의 상한. 데스크탑과 같은 값.
995
+ * 2→4: 출력검증 문제는 사람에게 못 묻고 모델 단독 교정만 가능한데, 이제 메시지가 정확한
996
+ * checks[] 항목을 그대로 주므로(추가만 하면 됨) 몇 번 더 주면 대개 수렴한다. */
997
+ const MAX_SELF_CORRECTIONS = 4;
954
998
 
955
999
  module.exports = {
956
1000
  BLUEPRINT_SCHEMA, MAX_QUESTIONS_PER_TURN, MAX_INTERVIEW_ROUNDS, MAX_REPEATS,
957
1001
  startInterview, recordAnswers, buildInterviewPrompt, parseInterviewTurn, humanSchedule,
958
1002
  MAX_SELF_CORRECTIONS, weakenedAgainstLastAttempt,
959
- validateBlueprint, buildGraphFromBlueprint, branchLabel, describeBranches,
1003
+ validateBlueprint, autofillOutputChecks, buildGraphFromBlueprint, branchLabel, describeBranches,
960
1004
  };
@@ -27,7 +27,7 @@ const path = require("node:path");
27
27
  const fs = require("node:fs");
28
28
  const { spawn } = require("node:child_process");
29
29
  const { Ui } = require("../agentlas-ui.cjs");
30
- const { truncateWidth, visWidth, wrapWidth } = require("../agentlas-composer.cjs");
30
+ const { truncateWidth, visWidth, wrapWidth } = require("../ui/width.cjs");
31
31
  const coreHarness = require("../agentlas-core-harness.cjs");
32
32
  const { userDataDir } = require("../core/paths.cjs");
33
33
 
@@ -194,7 +194,13 @@ function create(ctx, deps = {}) {
194
194
  const spawnCoreModule = deps.spawnCoreModule || coreHarness.spawnCoreModule;
195
195
  const lang = () => (ctx && ctx.lang) || "en";
196
196
 
197
+ /*
198
+ * pi-tui 이행 정지작업 (D3 Phase 1-2, 2026-08-11): ctx가 준 Ui가 있으면 그것을
199
+ * 쓴다. 자체 생성 Ui는 렌더러 교체 시 구 코드가 stdout에 직접 써 프레임을
200
+ * 찢는 병렬 경로였다. 생성은 주입이 없을 때의 폴백으로만 남긴다.
201
+ */
197
202
  function newUi(uiLang) {
203
+ if (ctx && ctx.uiInstance) return ctx.uiInstance;
198
204
  return new Ui({ lang: uiLang || lang() });
199
205
  }
200
206
 
@@ -73,6 +73,8 @@ function modelRoutingReceiptPath() {
73
73
  function buildStormDeps(ctx = {}) {
74
74
  return {
75
75
  prefsLang: () => ctx.lang || "en",
76
+ // Phase 1-2: 주입 Ui 관통 (자체 생성 방지)
77
+ uiInstance: ctx.uiInstance || null,
76
78
  out: typeof ctx.out === "function" ? ctx.out : (s) => process.stdout.write(`${s}\n`),
77
79
  resolveRuntime: resolveWorkforceRuntime,
78
80
  listAvailableRuntimes,
@@ -36,7 +36,13 @@ function create(deps) {
36
36
  const D = deps;
37
37
  const { swarmRun } = require("./swarm.cjs").create(D);
38
38
 
39
+ /*
40
+ * pi-tui 이행 정지작업 (D3 Phase 1-2, 2026-08-11): ctx가 준 Ui가 있으면 그것을
41
+ * 쓴다. 자체 생성 Ui는 렌더러 교체 시 구 코드가 stdout에 직접 써 프레임을
42
+ * 찢는 병렬 경로였다. 생성은 주입이 없을 때의 폴백으로만 남긴다.
43
+ */
39
44
  function newUi(lang) {
45
+ if (D.uiInstance) return D.uiInstance;
40
46
  return new Ui({ lang: lang || D.prefsLang() });
41
47
  }
42
48
 
@@ -78,7 +84,7 @@ function create(deps) {
78
84
  return { ok: false };
79
85
  }
80
86
  if (goal.startsWith("-")) {
81
- ui.error("goal cannot start with '-'.");
87
+ ui.error("goal cannot start with '-'.", { reveal: true });
82
88
  return { ok: false };
83
89
  }
84
90
  const cwd = ctx.cwd || (typeof D.projectCwd === "function" ? D.projectCwd() : D.runCwd());
@@ -86,7 +92,7 @@ function create(deps) {
86
92
  try {
87
93
  executionHarness = await loadCoreStormbreakerHarness(cwd);
88
94
  } catch (error) {
89
- ui.error(`Stormbreaker Core harness unavailable: ${String((error && error.message) || error).slice(0, 400)}`);
95
+ ui.error(`Stormbreaker Core harness unavailable: ${String((error && error.message) || error).slice(0, 400)}`, { reveal: true });
90
96
  return { ok: false, error: "stormbreaker-core-harness-unavailable" };
91
97
  }
92
98
  const args = ["route", goal, "--project", cwd, "--runtime", "terminal"];
@@ -127,7 +127,13 @@ function parseSwarmOutput(text) {
127
127
  function create(deps) {
128
128
  const D = deps;
129
129
 
130
+ /*
131
+ * pi-tui 이행 정지작업 (D3 Phase 1-2, 2026-08-11): ctx가 준 Ui가 있으면 그것을
132
+ * 쓴다. 자체 생성 Ui는 렌더러 교체 시 구 코드가 stdout에 직접 써 프레임을
133
+ * 찢는 병렬 경로였다. 생성은 주입이 없을 때의 폴백으로만 남긴다.
134
+ */
130
135
  function newUi(lang) {
136
+ if (D.uiInstance) return D.uiInstance;
131
137
  return new Ui({ lang: lang || D.prefsLang() });
132
138
  }
133
139
 
@@ -146,7 +152,7 @@ function create(deps) {
146
152
  // Core 하네스 없이 stormbreaker 모드 진입 금지 — 로컬 모조 프롬프트로 대체하는
147
153
  // 것은 계약 위반이다(모델 실행 전에 실패해야 한다).
148
154
  if (stormbreaker && (!executionHarness || typeof executionHarness.system_prompt !== "string")) {
149
- ui.error("Stormbreaker requires the canonical Goal + UltraCode harness from Agentlas Core.");
155
+ ui.error("Stormbreaker requires the canonical Goal + UltraCode harness from Agentlas Core.", { reveal: true });
150
156
  return { ok: false, error: "stormbreaker-core-harness-unavailable" };
151
157
  }
152
158
  const coreHarnessPrompt = executionHarness && executionHarness.system_prompt;
@@ -307,7 +313,7 @@ function create(deps) {
307
313
  if (!planned) {
308
314
  ui.error(ui.lang === "ko"
309
315
  ? "플래너가 유효한 실행 계획(JSON)을 내지 못했습니다 — 정지합니다 (조용한 폴백 금지)."
310
- : "The planner did not produce a valid plan JSON — stopping (no silent fallback).");
316
+ : "The planner did not produce a valid plan JSON — stopping (no silent fallback).", { reveal: true });
311
317
  return { ok: false, reason: "invalid_plan_json" };
312
318
  }
313
319
  if (planned) {
@@ -376,7 +382,7 @@ function create(deps) {
376
382
  ui.line("");
377
383
  ui.info(`tasks: ${tasks.length} · done: ${done.length} · failed: ${failed}`);
378
384
  if (!done.length) {
379
- ui.error(ui.lang === "ko" ? "스웜이 완료한 작업이 없습니다." : "The swarm completed no work.");
385
+ ui.error(ui.lang === "ko" ? "스웜이 완료한 작업이 없습니다." : "The swarm completed no work.", { reveal: true });
380
386
  return { ok: false };
381
387
  }
382
388
 
@@ -411,7 +417,7 @@ function create(deps) {
411
417
  );
412
418
  } catch (e) {
413
419
  ui.stopSpinner();
414
- ui.error("Synthesis failed: " + String((e && e.message) || e).slice(0, 200));
420
+ ui.error("Synthesis failed: " + String((e && e.message) || e).slice(0, 200), { reveal: true });
415
421
  finalText = pieces;
416
422
  }
417
423
  ui.stopSpinner();
@@ -435,7 +441,7 @@ function create(deps) {
435
441
  const strayFlag = rest.find((token) => String(token).startsWith("-"));
436
442
  if (strayFlag) {
437
443
  const ui = executionContext.ui || newUi();
438
- ui.error(`unknown option ${strayFlag} — swarm accepts: --parallel N | -n N, --runtime <kind>`);
444
+ ui.error(`unknown option ${strayFlag} — swarm accepts: --parallel N | -n N, --runtime <kind>`, { reveal: true });
439
445
  process.exitCode = 1;
440
446
  return { ok: false, error: "unknown-option" };
441
447
  }