agentlas 1.0.36 → 1.0.38

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,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.38 — 2026-08-11
4
+
5
+ Silence was the worst failure mode — this release makes failures speak.
6
+
7
+ - The presentation boundary (`Ui.error`) now has conditions. Machine-coded
8
+ messages (usage guidance, honest stops, server relays) pass through; only
9
+ uncoded raw provider text is still replaced by the neutral recovery line.
10
+ Previously every failure — including usage help for eight argless slash
11
+ commands — collapsed into the same "One is recovering" sentence.
12
+ - `/s` `/switch` `/kill` `/rm` `/runtime` `/model` `/effort` `/permission`
13
+ without arguments now print their usage line instead of a recovery notice.
14
+ - Workforce sign-in expiry is relayed honestly: the server's `auth_required`
15
+ guidance reaches the user (run `agentlas login`), instead of being
16
+ misreported as an invalid continuity receipt and then swallowed.
17
+ - `doctor` verifies the cloud session against the server instead of only
18
+ checking that a session file exists. Expired sessions are reported as a
19
+ warning with the login hint; offline is reported as "unverified", never as
20
+ a false all-clear. `--json` output remains observation-only.
21
+ - Authored guidance in storm/swarm/workforce flows (planner refusals, unknown
22
+ options, persisted receipt issues) is no longer swallowed by the boundary.
23
+ - `npm run sync:architecture` works again — the script was restored to
24
+ `scripts/` where its relative paths are correct.
25
+
26
+ ## 1.0.37 — 2026-08-10
27
+
28
+ Agentlas One can now carry its owner-bound identity and curated memory tickets
29
+ across Terminal sessions without turning the agent into the owner of a project.
30
+
31
+ - When One is explicitly enabled, Terminal loads its bounded directive at the
32
+ per-turn system boundary and forwards only `agent_repo` and `user_identity`
33
+ memory candidates to One's existing ledger. Project memory stays with the
34
+ project, and a missing or disabled One workspace remains a no-op.
35
+ - The Memory Events parser accepts both the canonical ticket envelope and the
36
+ legacy array form, so valid candidates are no longer silently discarded.
37
+ - Global `--json` output now reaches doctor, list, roles, Cloud restore, and
38
+ upload commands consistently; machine-readable output is no longer prefixed
39
+ by human status text.
40
+ - The built-in architecture projection includes Agentlas One as a hidden
41
+ orchestrator rather than a top-level project or user-facing worker.
42
+
3
43
  ## 1.0.34 — 2026-08-06
4
44
 
5
45
  Telegram, standalone. The terminal can now connect a Telegram bot with no desktop app.
@@ -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.6.0",
2
+ "version": "1.7.0",
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",
@@ -138,6 +138,18 @@
138
138
  "visibility": "background",
139
139
  "tone": "amber",
140
140
  "systemPrompt": "# Task Bias Curator (Agentlas built-in)\n\nYou reduce TASK BIAS in multi-surface projects — the tendency to keep working on\nsurfaces that are recent, salient, or easy to measure while other surfaces stay\nuninspected. You are a SECOND-ORDER control role: you adjust the rules of work\nallocation and evidence review; you do not implement product work yourself, and you\ncannot mark a node \"complete\".\n\n## External state: the AI Sitemap\nThe project's shared external state lives in .agentlas/sitemap.json. Each\nnode carries: node_id, kind, status (unknown|todo|in_progress|blocked|validated|revalidate),\ncompletion_score (0..1, evidence-backed), risk_level, last_modified, last_tested,\ndependencies, acceptance_checks, evidence, provisional.\n\n## What you do\n1. Read/maintain the sitemap. Create provisional nodes for newly discovered surfaces.\n2. Choose the next bounded task from a VISIBLE priority policy, not recent chat context:\n prioritize high risk, low completion_score, stale last_tested, and blocking dependencies.\n3. Audit for bias: which surfaces are over-worked vs never inspected? Name them.\n4. Audit validation: flag completion claims without evidence or with weak evidence;\n require revalidation and name the missing evidence.\n5. Produce a compact, reversible curator decision record. Escalate mission-level changes\n to the user.\n\n## Boundaries\nCannot mark a node complete. Cannot erase evidence (only supersede it with a logged\ndecision). Cannot expand the project mission without explicit user approval.\n\nKeep outputs small: a policy/priority recommendation, a revalidation request, a\nsitemap update proposal, or a provisional-node decision."
141
+ },
142
+ {
143
+ "id": "builtin-agentlas-orchestrator",
144
+ "slug": "agentlas-one",
145
+ "name": "Agentlas One",
146
+ "nameEn": "Agentlas One",
147
+ "tagline": "오너 전속 개인 에이전트 — 전문가·도구·기억을 필요할 때만 꺼내 쓴다",
148
+ "taglineEn": "Owner-bound personal agent that pulls in specialists, tools, and memory on demand",
149
+ "role": "orchestrator",
150
+ "visibility": "background",
151
+ "tone": "green",
152
+ "systemPrompt": "# Agentlas One (Agentlas built-in)\n\n너는 채팅 어시스턴트가 아니라 오너 전속 개인 에이전트다. 세션·프로젝트·런타임을 넘어\n같은 정체성을 유지한다. 자기소개를 반복하지 않는다.\n\n## 일하는 법\n- 전문가가 필요하면 새로 고르기 전에 이미 묶인 로스터를 재사용한다. 채용은 가산이다.\n- \"못 한다\"고 말하기 전에 보유 수단(로컬 에이전트·오너 클라우드·Hub·플러그인)을 실제로 조회한다.\n 호출하지 않은 도구를 호출한 척하지 않는다.\n- 되돌리기 어려운 일(파일 삭제·발송·결제·공개) 직전에는 기억보다 실측을 우선한다.\n 확인이 안 되면 진행하지 말고 오너에게 묻는다.\n- 기억이 서로 모순되면 하나를 골라 단정하지 말고 충돌을 그대로 말한다.\n- 막히면 멈추지 말고 남은 수단을 순서대로 시도하고, 다 막히면 어디서 왜 막혔는지 기계 근거로 한 줄.\n\n## 기억\ndurable 기억을 직접 쓰지 않는다. 관찰은 Memory Events 로 내고 런타임이 티켓으로 포장해\n큐레이터에 넘긴다. 증거 없는 fact/decision/procedure 는 hypothesis 다.\n\n## 경계\n허브·클라우드에 업로드되지 않는다. 오너의 원시 기억·자격증명·전사를 외부로 내보내지 않는다."
141
153
  }
142
154
  ]
143
155
  }
@@ -7,7 +7,10 @@ const { runCloud } = require("../cloud-assets/commands.cjs");
7
7
 
8
8
  async function run(ctx, args) {
9
9
  try {
10
- return await runCloud(ctx, args);
10
+ const commandArgs = ctx.output?.format === "json" && !args.includes("--json")
11
+ ? [...args, "--json"]
12
+ : args;
13
+ return await runCloud(ctx, commandArgs);
11
14
  } catch (e) {
12
15
  ctx.err(String((e && e.message) || e));
13
16
  return 1;
@@ -24,10 +24,10 @@ 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
- if (args.includes("--json")) {
30
+ if (ctx.output?.format === "json" || args.includes("--json")) {
31
31
  const db = ctx.db();
32
32
  const clis = listAvailableCliRuntimes().map((c) => ({ kind: c.kind, path: c.path, authEvidence: runtimeAuthEvidence(c.kind).status }));
33
33
  const active = activeRuntimeRow(db);
@@ -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("");
@@ -36,7 +36,7 @@ function run(ctx, args = []) {
36
36
 
37
37
  // clig.dev: 스크립트 소비자는 사람용 표를 파싱하게 두지 말 것 — --json 은
38
38
  // 사람용 출력과 같은 사실을 기계 계약으로 준다.
39
- if (args.includes("--json")) {
39
+ if (ctx.output?.format === "json" || args.includes("--json")) {
40
40
  const orchestrator = resolvedModelRole(db, "orchestrator");
41
41
  const worker = resolvedModelRole(db, "worker");
42
42
  ctx.out(JSON.stringify({ agents, firms, modelRoles: { orchestrator, worker } }, null, 2));
@@ -38,7 +38,7 @@ function fmt(selection, en) {
38
38
  function show(ctx, args = []) {
39
39
  const en = ctx.lang === "en";
40
40
  const db = ctx.db();
41
- if (args.includes("--json")) {
41
+ if (ctx.output?.format === "json" || args.includes("--json")) {
42
42
  ctx.out(JSON.stringify({
43
43
  orchestrator: resolvedModelRole(db, "orchestrator"),
44
44
  worker: resolvedModelRole(db, "worker"),
@@ -177,6 +177,7 @@ function set(ctx, args) {
177
177
 
178
178
  function run(ctx, args = []) {
179
179
  const en = ctx.lang === "en";
180
+ if (ctx.output?.format === "json" && !args.includes("--json")) args = [...args, "--json"];
180
181
  const [sub, ...rest] = args;
181
182
  if (!sub || sub === "show" || sub === "list" || sub === "--json") return show(ctx, sub === "--json" ? ["--json"] : rest);
182
183
  if (sub === "set") return set(ctx, rest);
@@ -8,7 +8,10 @@ const { runUpload } = require("../cloud-assets/commands.cjs");
8
8
 
9
9
  async function run(ctx, args) {
10
10
  try {
11
- return await runUpload(ctx, args);
11
+ const commandArgs = ctx.output?.format === "json" && !args.includes("--json")
12
+ ? [...args, "--json"]
13
+ : args;
14
+ return await runUpload(ctx, commandArgs);
12
15
  } catch (e) {
13
16
  ctx.err(String((e && e.message) || e));
14
17
  return 1;
@@ -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
 
@@ -96,7 +96,18 @@ function parseMemoryEventsCli(text) {
96
96
  const after = text.slice(idx + heading.length);
97
97
  const fence = after.match(/```(?:json)?\s*([\s\S]*?)```/);
98
98
  let events = [];
99
- if (fence) { try { const d = JSON.parse(fence[1].trim()); if (Array.isArray(d)) events = d; } catch { /* ignore */ } }
99
+ // 봉투 형태를 모두 받는다. Desktop `electron/memory/events.ts:128,156` 같은 계약이다.
100
+ // · 정본: {"schema_version":"agentlas.memory-ticket.v1","candidates":[...]} ← 이미터가 지시하는 형태
101
+ // · 레거시: [...] (최상위 배열)
102
+ // ★배열만 받던 시절 정본 봉투는 **조용히 버려졌다**(실측: 터미널이 후보 5건을 냈는데 파서 결과 0건).
103
+ // 형태가 낯설면 버리지 말고, 최소한 왜 못 읽었는지 남길 수 있게 둘 다 통과시킨다.
104
+ if (fence) {
105
+ try {
106
+ const d = JSON.parse(fence[1].trim());
107
+ if (Array.isArray(d)) events = d;
108
+ else if (d && typeof d === "object" && Array.isArray(d.candidates)) events = d.candidates;
109
+ } catch { /* ignore */ }
110
+ }
100
111
  let cut = text.length;
101
112
  if (fence && fence.index != null) cut = idx + heading.length + fence.index + fence[0].length;
102
113
  const before = text.slice(0, idx).replace(/<!--\s*$/u, "");
@@ -18,6 +18,75 @@ const memoryCurate = require("../memory-cli/curate.cjs");
18
18
  const automationStore = require("../automation/store.cjs");
19
19
  const schedule = require("../automation/schedule.cjs");
20
20
 
21
+ /** One 이 가져가는 스코프. 프로젝트 스코프는 프로젝트에 남는다. */
22
+ const ONE_SCOPES = new Set(["agent_repo", "user_identity"]);
23
+
24
+ /**
25
+ * Agentlas One 서랍(`~/.agentlas/one/.agentlas/memory-tickets.jsonl`)으로 후보를 넘긴다.
26
+ *
27
+ * One 은 프로젝트를 넘나드는 정체성이라 agent_repo/user_identity 만 가져간다.
28
+ * One 이 꺼져 있거나 서랍이 없으면 아무것도 하지 않는다 — 없는 폴더를 만들지 않는다.
29
+ * 실패해도 턴을 죽이지 않는다(펜스 적용 계약과 동일).
30
+ */
31
+ function forwardToOne(events) {
32
+ try {
33
+ const fs = require("node:fs");
34
+ const os = require("node:os");
35
+ const path = require("node:path");
36
+ const root = process.env.AGENTLAS_ONE_DIR || path.join(os.homedir(), ".agentlas", "one");
37
+ const state = JSON.parse(fs.readFileSync(path.join(root, "state.json"), "utf8"));
38
+ if (!state || state.on !== true) return 0;
39
+ const ledger = path.join(root, ".agentlas", "memory-tickets.jsonl");
40
+ if (!fs.existsSync(ledger)) return 0;
41
+
42
+ // 이미 올라온 내용은 다시 넣지 않는다(같은 계약을 One 쪽 emit_ticket 도 쓴다).
43
+ const seen = new Set();
44
+ for (const line of fs.readFileSync(ledger, "utf8").split("\n")) {
45
+ if (!line.trim()) continue;
46
+ try {
47
+ const row = JSON.parse(line);
48
+ const text = String((row.candidate || {}).content || "");
49
+ if (text) seen.add(text.trim().toLowerCase().replace(/\s+/g, " "));
50
+ } catch { /* 깨진 줄은 건너뛴다 */ }
51
+ }
52
+
53
+ let written = 0;
54
+ for (const raw of events) {
55
+ for (const candidate of (raw && Array.isArray(raw.candidates) ? raw.candidates : [raw])) {
56
+ if (!candidate || typeof candidate !== "object") continue;
57
+ const scope = String(candidate.suggested_scope || candidate.scope || "");
58
+ if (!ONE_SCOPES.has(scope)) continue;
59
+ const content = String(candidate.content || "").trim();
60
+ if (!content) continue;
61
+ const key = content.toLowerCase().replace(/\s+/g, " ");
62
+ if (seen.has(key)) continue;
63
+ seen.add(key);
64
+ const evidence = Array.isArray(candidate.evidence) ? candidate.evidence.slice(0, 8) : [];
65
+ fs.appendFileSync(ledger, JSON.stringify({
66
+ schemaVersion: "agentlas.one-workspace.v1",
67
+ ticketId: `one-tkt-${Date.now()}-${written}`,
68
+ agentId: "builtin-agentlas-one",
69
+ turnKey: "",
70
+ source: "terminal-memory-events",
71
+ state: "queued",
72
+ candidate: {
73
+ type: String(candidate.memory_kind || candidate.type || "hypothesis"),
74
+ scope,
75
+ content: content.slice(0, 600),
76
+ evidence,
77
+ },
78
+ downgraded: false,
79
+ createdAt: new Date().toISOString().replace(/\.\d+Z$/, "Z"),
80
+ }) + "\n", "utf8");
81
+ written += 1;
82
+ }
83
+ }
84
+ return written;
85
+ } catch {
86
+ return 0;
87
+ }
88
+ }
89
+
21
90
  /**
22
91
  * @param {import('./session.cjs').Session} session 방금 턴을 끝낸 세션
23
92
  * @param {object} parsed parseReplyFences 결과
@@ -69,6 +138,10 @@ function applyReplyFences(session, parsed, opts = {}) {
69
138
  written: ctx.curatedMemories.length,
70
139
  permission: session.permission,
71
140
  };
141
+ // Agentlas One 이 켜져 있으면 에이전트 스코프 후보를 One 서랍에도 티켓으로 넘긴다.
142
+ // 프로젝트 스코프는 여기 남기고 옮기지 않는다 — One 은 프로젝트를 넘나드는 정체성이라
143
+ // agent_repo/user_identity 만 One 의 것이다(기획 2.2 스코프 경계).
144
+ receipts.memory.one = forwardToOne(parsed.memoryEvents);
72
145
  // read 권한 턴 = durable 쓰기 0 — 영수증 이벤트만 남는다.
73
146
  record({ type: "memory-curated", ...receipts.memory });
74
147
  }
@@ -185,4 +258,5 @@ function applyReplyFences(session, parsed, opts = {}) {
185
258
  return receipts;
186
259
  }
187
260
 
188
- module.exports = { applyReplyFences };
261
+ // forwardToOne One 서랍 전달의 유일한 지점이라 계약 테스트가 직접 잴 수 있게 함께 노출한다.
262
+ module.exports = { applyReplyFences, forwardToOne };
@@ -180,6 +180,29 @@ function cliMemoryContext(db, projectPath, agentId = null, task = "") {
180
180
  * 최종 시스템 프롬프트 조립. ctx = { lang, projectPath, agentId, turnId, permission }.
181
181
  * withEmitter=false 는 이미터/리마인더 없이(캡처·판정 등 내부 턴용).
182
182
  */
183
+ /**
184
+ * Agentlas One 지시문. 켜져 있을 때만, 그리고 정본 파일이 실재할 때만 싣는다.
185
+ * 상태 파일이 없으면 조용히 빈 문자열 — 꺼진 One 의 지시문을 흘리지 않는다.
186
+ */
187
+ function loadOneDirective() {
188
+ try {
189
+ const root = process.env.AGENTLAS_ONE_DIR
190
+ || require("node:path").join(require("node:os").homedir(), ".agentlas", "one");
191
+ const fs = require("node:fs");
192
+ const path = require("node:path");
193
+ const state = JSON.parse(fs.readFileSync(path.join(root, "state.json"), "utf8"));
194
+ if (!state || state.on !== true) return "";
195
+ const text = fs.readFileSync(path.join(root, "directive.md"), "utf8").trim();
196
+ // 상한을 둔다 — 지시문이 남의 토큰 예산을 잠식하면 안 된다(메모리 예산과 같은 규칙).
197
+ return text.length > ONE_DIRECTIVE_MAX_CHARS ? text.slice(0, ONE_DIRECTIVE_MAX_CHARS) : text;
198
+ } catch {
199
+ return "";
200
+ }
201
+ }
202
+
203
+ /** [튜닝값, 근거 없음] — 현재 정본 지시문이 약 3.7KB 라 두 배 여유를 둔다. */
204
+ const ONE_DIRECTIVE_MAX_CHARS = 8000;
205
+
183
206
  function augmentSystem(db, baseSystem, ctx, withEmitter, request = "") {
184
207
  const arch = loadArch();
185
208
  let sys = baseSystem || "";
@@ -188,6 +211,10 @@ function augmentSystem(db, baseSystem, ctx, withEmitter, request = "") {
188
211
  sys = responseDirective(lang) + (sys ? "\n\n" + sys : "");
189
212
  const connectionSkill = loadGlobalConnectionSkill();
190
213
  if (connectionSkill) sys += "\n\n" + connectionSkill;
214
+ // Agentlas One 이 켜져 있으면 그 지시문을 싣는다. R4 기준 터미널의 "매 턴 주입 지점"이 여기다.
215
+ // 정본은 `~/.agentlas/one/directive.md` 하나 — CLAUDE.md/AGENTS.md 의 마커 블록은 그 사본이다.
216
+ const oneDirective = loadOneDirective();
217
+ if (oneDirective) sys += "\n\n" + oneDirective;
191
218
  const mem = cliMemoryContext(db, ctx && ctx.projectPath, ctx && ctx.agentId, request);
192
219
  if (mem) sys += "\n\n" + mem;
193
220
  // 도구 접근 고지 — 터미널에는 이게 아예 없었다. 도구가 붙지 않은 턴에서 CLI는 아무
@@ -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,