diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json
index e971f8e..f3a270f 100644
--- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json
+++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
- "createdAt": 1778682718199,
+ "createdAt": 1778684049645,
"modelVersion": "unknown"
}
\ No newline at end of file
diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json
index 89fce0f..8f780e6 100644
--- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json
+++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json
@@ -1,5 +1,5 @@
{
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
- "createdAt": 1778682718198,
+ "createdAt": 1778684049641,
"modelVersion": "unknown"
}
\ No newline at end of file
diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json
index 242d4c8..2a4c637 100644
--- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json
+++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json
@@ -1,5 +1,5 @@
{
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
- "createdAt": 1778682718197,
+ "createdAt": 1778684049636,
"modelVersion": "unknown"
}
\ No newline at end of file
diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json
index 4b1da63..ed5eb8c 100644
--- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json
+++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json
@@ -1,5 +1,5 @@
{
- "result": "---\nid: stress_conflict_1778682718185\ndate: 2026-05-13T14:31:58.199Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (2ms)\n",
- "createdAt": 1778682718199,
+ "result": "---\nid: stress_conflict_1778684049621\ndate: 2026-05-13T14:54:09.649Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (5ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)\n",
+ "createdAt": 1778684049649,
"modelVersion": "unknown"
}
\ No newline at end of file
diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778682718185.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778684049621.json
similarity index 80%
rename from .astra/tests/stress/.astra/missions/stress_conflict_1778682718185.json
rename to .astra/tests/stress/.astra/missions/stress_conflict_1778684049621.json
index 0558b24..bdcea08 100644
--- a/.astra/tests/stress/.astra/missions/stress_conflict_1778682718185.json
+++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778684049621.json
@@ -1,8 +1,8 @@
{
- "missionId": "stress_conflict_1778682718185",
+ "missionId": "stress_conflict_1778684049621",
"status": "completed",
- "startTime": "2026-05-13T14:31:58.185Z",
- "totalElapsedMs": 14,
+ "startTime": "2026-05-13T14:54:09.621Z",
+ "totalElapsedMs": 29,
"results": {
"planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
@@ -18,28 +18,28 @@
"to": "planner",
"durationMs": 11,
"message": "전략 수립 중...",
- "ts": "2026-05-13T14:31:58.196Z"
+ "ts": "2026-05-13T14:54:09.632Z"
},
{
"from": "planner",
"to": "researcher",
- "durationMs": 1,
+ "durationMs": 5,
"message": "핵심 정보 수집 및 분석 중...",
- "ts": "2026-05-13T14:31:58.197Z"
+ "ts": "2026-05-13T14:54:09.637Z"
},
{
"from": "researcher",
"to": "writer",
- "durationMs": 2,
+ "durationMs": 8,
"message": "최종 리포트 작성 및 편집 중...",
- "ts": "2026-05-13T14:31:58.199Z"
+ "ts": "2026-05-13T14:54:09.645Z"
},
{
"from": "writer",
"to": "completed",
- "durationMs": 0,
+ "durationMs": 5,
"message": "미션 완료",
- "ts": "2026-05-13T14:31:58.199Z"
+ "ts": "2026-05-13T14:54:09.650Z"
}
],
"resilienceMetrics": {
diff --git a/PATCHNOTES.md b/PATCHNOTES.md
index 1002ca6..1e4d1c9 100644
--- a/PATCHNOTES.md
+++ b/PATCHNOTES.md
@@ -1,5 +1,14 @@
# Astra Patch Notes
+## v2.0.5 (2026-05-13)
+### 📢 Telegram Business Reporting & Core Resilience
+- **텔레그램 비즈니스 리포팅 도입:** 비즈니스 에이전트의 성과를 실시간으로 보고하는 `telegramReport.ts`를 추가하여 원격 모니터링 기능을 강화했습니다.
+- **에이전트 엔진 복원력 강화:** `agent.ts` 및 `dispatcher.ts` 내의 예외 처리 로직을 보강하여 복잡한 자율 미션 수행 시의 안정성을 높였습니다.
+- **사이드바 상태 관리 최적화:** `sidebarProvider.ts`와 `sidebar.js`를 수정하여 세션 전환 및 대화 초기화 시의 반응성을 개선했습니다.
+- **신규 패키징:** `astra-2.0.5.vsix` 패키지를 통해 텔레그램 연동 보고와 강화된 엔진 안정성을 통합 배포합니다.
+
+---
+
## v2.0.4 (2026-05-13)
### ⚡ Advanced Business Orchestration & UI Polishing
- **비즈니스 에이전트 고도화:** `companyConfig.ts` 및 `promptBuilder.ts` 수정을 통해 CEO 에이전트의 목표 설정 및 컨텍스트 주입 로직을 정교화했습니다.
diff --git a/media/sidebar.js b/media/sidebar.js
index 7fe2007..d9b3bf6 100644
--- a/media/sidebar.js
+++ b/media/sidebar.js
@@ -1693,6 +1693,19 @@
card.className += ' report';
card.innerHTML = `
🧭 CEO 보고서${ev.ok ? '' : ' (fallback)'}
${fmt(ev.report || '')}
`;
+ } else if (ev.phase === 'telegram-mirror') {
+ // Reflect whether the secretary actually mirrored the round
+ // to Telegram. `ok === null` = the user hasn't opted in to
+ // Telegram at all (no token / chat id / enabled). We render
+ // that as a quiet line instead of an error to avoid nagging.
+ if (ev.ok === true) {
+ card.innerHTML = '📱 영숙이 텔레그램에 보고 완료
';
+ } else if (ev.ok === false) {
+ card.innerHTML = `⚠️ 텔레그램 보고 실패${ev.reason ? ` — ${escAttr(ev.reason)}` : ''}
`;
+ } else {
+ // null → not configured. Skip rendering entirely to keep chat clean.
+ return;
+ }
} else if (ev.phase === 'session-saved') {
card.innerHTML = `세션 저장 완료 — 클릭하여 열기
`;
card.style.cursor = 'pointer';
diff --git a/package.json b/package.json
index 1ccf2db..453889f 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "astra",
"displayName": "Astra",
"description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.",
- "version": "2.0.4",
+ "version": "2.0.5",
"publisher": "g1nation",
"license": "MIT",
"icon": "assets/icon.png",
diff --git a/src/agent.ts b/src/agent.ts
index 0be0e3d..06c1fe2 100644
--- a/src/agent.ts
+++ b/src/agent.ts
@@ -3054,6 +3054,35 @@ export class AgentExecutor {
return candidates;
}
+ /**
+ * Public entry point for callers that need to apply ConnectAI's action
+ * tags (``, ``, ``, …) to arbitrary
+ * text without going through the full `handlePrompt` pipeline.
+ *
+ * The 1인 기업 dispatcher uses this so specialist outputs that contain
+ * action tags actually take effect on disk — without it, agents would
+ * "claim" to create files but nothing would be written, which is the
+ * exact symptom the user reported.
+ *
+ * Returns the action report (`["✅ Created: …", "📂 Listed: …", …]`) so
+ * the caller can surface it back to the user. Errors inside individual
+ * actions are converted into report entries rather than thrown, matching
+ * the behaviour of the internal call site.
+ */
+ public async executeActionTagsOnText(aiMessage: string): Promise {
+ const cfg = getConfig();
+ const rootPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
+ || cfg.localBrainPath
+ || process.cwd();
+ const activeBrain = getActiveBrainProfile();
+ try {
+ return await this.executeActions(aiMessage, rootPath, activeBrain);
+ } catch (e: any) {
+ logError('executeActionTagsOnText failed.', { error: e?.message ?? String(e) });
+ return [`❌ Action 실행 중 오류: ${e?.message ?? e}`];
+ }
+ }
+
private async executeActions(aiMessage: string, rootPath: string, activeBrain: BrainProfile): Promise {
const report: string[] = [];
let brainModified = false;
diff --git a/src/features/company/dispatcher.ts b/src/features/company/dispatcher.ts
index 068171b..2b0e6db 100644
--- a/src/features/company/dispatcher.ts
+++ b/src/features/company/dispatcher.ts
@@ -50,7 +50,8 @@ import {
writeReport,
writeSessionJson,
} from './sessionStore';
-import { AgentTurnOutput, CompanyTaskPlan, SessionResult } from './types';
+import { buildTelegramReporter, formatCompanyTelegramReport } from './telegramReport';
+import { AgentTurnOutput, CompanyState, CompanyTaskPlan, SessionResult } from './types';
/** Trim length applied when an agent's output is fed into the next agent. */
const PEER_OUTPUT_BUDGET = 1500;
@@ -68,6 +69,13 @@ export type CompanyTurnEvent =
| { phase: 'agent-done'; agentId: string; output: AgentTurnOutput; index: number; total: number }
| { phase: 'report-start' }
| { phase: 'report-done'; report: string; ok: boolean }
+ /**
+ * Emitted after the secretary attempts to mirror the CEO report to
+ * Telegram. `ok=true` ⇒ delivered. `ok=false` ⇒ delivery failed (network
+ * / API error). `ok=null` ⇒ no mirror was attempted because the user
+ * hasn't opted in (no token, no chat id, or `telegram.enabled=false`).
+ */
+ | { phase: 'telegram-mirror'; ok: boolean | null; reason?: string }
| { phase: 'session-saved'; sessionDir: string }
| { phase: 'aborted'; reason: string };
@@ -78,6 +86,15 @@ export interface DispatcherDeps {
ai: IAIService;
/** Default model to fall back to when an agent has no override. */
defaultModel: string;
+ /**
+ * Apply ConnectAI's action-tag executor to the specialist's raw response.
+ * Without this hook, agent outputs containing `` etc. would
+ * be shown to the user as a *claim* of file creation but nothing would
+ * actually land on disk. We thread it through deps (rather than
+ * importing AgentExecutor directly) so the dispatcher stays free of
+ * sidebar / agent.ts dependencies for unit testability.
+ */
+ executeActionTags?: (text: string) => Promise;
/** Per-call cancellation. The sidebar's Stop button flips this. */
signal?: AbortSignal;
/** Optional event sink for the webview. Receives events synchronously. */
@@ -160,6 +177,35 @@ export async function runCompanyTurn(
writeReport(sessionDir, reportResult.report);
emit({ phase: 'report-done', report: reportResult.report, ok: reportResult.ok });
+ // ── Phase 3.5: Secretary mirror to Telegram ──
+ // Origin parity (Connect_origin extension.ts ~line 20620): after the CEO
+ // synthesizes the round, the Secretary (영숙) ships a digest to the user's
+ // Telegram chat. Strictly best-effort — failures never break the turn.
+ let telegramOk: boolean | null = null;
+ let telegramReason: string | undefined;
+ try {
+ const reporter = await buildTelegramReporter(deps.context);
+ if (!reporter) {
+ telegramReason = 'no-config'; // opt-in off, no token, or no chat id
+ } else {
+ const tgText = formatCompanyTelegramReport({
+ state,
+ userPrompt,
+ plan: plannerResult.plan,
+ outputs,
+ report: reportResult.report,
+ sessionTimestamp: timestamp,
+ });
+ telegramOk = await reporter(tgText);
+ if (!telegramOk) telegramReason = 'delivery-failed';
+ }
+ } catch (e: any) {
+ telegramOk = false;
+ telegramReason = e?.message ?? String(e);
+ logError('company.dispatcher: telegram mirror threw.', { error: telegramReason });
+ }
+ emit({ phase: 'telegram-mirror', ok: telegramOk, reason: telegramReason });
+
// ── Phase 4: persist + side effects ──
const result: SessionResult = {
timestamp, sessionDir,
@@ -232,12 +278,31 @@ async function _dispatchOne(
user: task,
model,
});
- const response = (result.content || '').trim();
+ const rawResponse = (result.content || '').trim();
+ // Apply ConnectAI's action-tag executor so ``,
+ // ``, ``, etc. emitted by the agent actually
+ // hit disk / shell. The report (e.g. "✅ Created: foo.py") is
+ // appended to the response so the user sees what really happened.
+ let finalResponse = rawResponse || '_(empty response)_';
+ if (rawResponse && deps.executeActionTags && _hasActionTag(rawResponse)) {
+ try {
+ const report = await deps.executeActionTags(rawResponse);
+ if (report.length > 0) {
+ finalResponse = `${rawResponse}\n\n---\n**Action 실행 결과:**\n${report.map((r) => `- ${r}`).join('\n')}`;
+ }
+ } catch (e: any) {
+ // Surface the failure but keep the agent's text — partial
+ // success is more useful than dropping the whole response.
+ const err = e?.message ?? String(e);
+ logError('company.dispatcher: action-tag execution failed.', { agentId, err });
+ finalResponse = `${rawResponse}\n\n---\n⚠️ Action 실행 실패: ${err}`;
+ }
+ }
return {
agentId, task,
- response: response || '_(empty response)_',
+ response: finalResponse,
durationMs: Date.now() - startedAt,
- error: response ? undefined : 'empty-response',
+ error: rawResponse ? undefined : 'empty-response',
};
} catch (e: any) {
const err = e?.message ?? String(e);
@@ -250,3 +315,13 @@ async function _dispatchOne(
};
}
}
+
+/**
+ * Cheap pre-check so we don't fire up the action-tag executor for every
+ * specialist response — only the ones that actually contain a recognised
+ * tag. Saves a workspace lookup + transaction-manager spin-up on the common
+ * case (the agent just talks).
+ */
+function _hasActionTag(text: string): boolean {
+ return /<\s*(?:create_file|edit_file|delete_file|read_file|list_files|list_brain|run_command|read_brain|reveal_in_explorer|open_file|glob|grep)\b/i.test(text);
+}
diff --git a/src/features/company/telegramReport.ts b/src/features/company/telegramReport.ts
new file mode 100644
index 0000000..47bf4da
--- /dev/null
+++ b/src/features/company/telegramReport.ts
@@ -0,0 +1,130 @@
+/**
+ * Telegram mirror for the secretary agent (영숙).
+ *
+ * After every company turn finishes, this helper takes the CEO synthesis +
+ * task list and pushes it to the user's Telegram chat — same behaviour as
+ * Connect_origin's `_handleCorporatePrompt` (lines ~20620): "Secretary
+ * automatically reports the round to the user." Connect AI had a Telegram
+ * integration but it was wired only for *inbound* messages — the company
+ * dispatcher we shipped earlier never called it.
+ *
+ * Design notes:
+ * - **Outbound-only**: no polling, no inbound side effects. Sends a single
+ * `sendMessage` per turn.
+ * - **Honours user opt-in**: requires `g1nation.telegram.enabled` to be
+ * true AND a stored bot token AND at least one allowed chat id. If any
+ * piece is missing, `buildTelegramReporter` returns `null` and the
+ * dispatcher silently skips the mirror — no log spam, no fake error.
+ * - **Per-call isolation**: each reporter fetches the token freshly via
+ * `context.secrets.get`, so rotating the secret in the settings UI
+ * takes effect on the *next* turn without a restart.
+ * - **Markdown-aware**: re-uses `TelegramHttpClient`, which already
+ * truncates to Telegram's 4096-char limit and falls back to plain text
+ * when the Markdown parser refuses a string.
+ */
+import * as vscode from 'vscode';
+import { logError, logInfo } from '../../utils';
+import { TelegramHttpClient } from '../../integrations/telegram/telegramClient';
+import { COMPANY_AGENTS } from './agents';
+import { AgentTurnOutput, CompanyState, CompanyTaskPlan } from './types';
+
+/** Same key the rest of the extension uses. Defined locally so this module is dependency-free. */
+const TELEGRAM_TOKEN_SECRET_KEY = 'g1nation.telegram.botToken';
+
+/** Maximum characters of the CEO report to embed verbatim in the Telegram message. */
+const REPORT_BUDGET = 2000;
+
+/** Reporter contract — call once per turn, returns whether delivery succeeded. */
+export type TelegramReporter = (text: string) => Promise;
+
+/**
+ * Build a Telegram reporter bound to the current user's bot config. Returns
+ * `null` when the user hasn't opted in (no token, no chat id, or
+ * `telegram.enabled` is false) — the dispatcher then treats it as "no
+ * mirror, nothing to do".
+ *
+ * The returned function targets the *first* allowed chat id. Users with
+ * multiple chats today still get one canonical destination; broader fan-out
+ * would be a feature add, not a port of origin's behaviour.
+ */
+export async function buildTelegramReporter(
+ context: vscode.ExtensionContext,
+): Promise {
+ const cfg = vscode.workspace.getConfiguration('g1nation');
+ if (!cfg.get('telegram.enabled', false)) return null;
+
+ const token = ((await context.secrets.get(TELEGRAM_TOKEN_SECRET_KEY)) || '').trim();
+ if (!token) {
+ logInfo('telegramReport: opt-in is on but no bot token stored; skipping mirror.');
+ return null;
+ }
+
+ const allowed = cfg.get('telegram.allowedChatIds', []) || [];
+ const chatId = allowed.find((id) => typeof id === 'number' && Number.isFinite(id) && id !== 0);
+ if (!chatId) {
+ logInfo('telegramReport: telegram enabled but allowedChatIds is empty; skipping mirror.');
+ return null;
+ }
+
+ // Build a thin client per turn — token capture happens once, so this
+ // doesn't keep a long-lived reference to the secret.
+ const client = new TelegramHttpClient({ getToken: () => token });
+
+ return async (text: string): Promise => {
+ if (!text || !text.trim()) return false;
+ try {
+ await client.sendMessage({ chatId, text, parseMode: 'Markdown' });
+ return true;
+ } catch (e: any) {
+ // Fallback to plain text — Telegram rejects malformed Markdown
+ // (unbalanced asterisks, etc.). The origin does the same thing
+ // in `sendTelegramLong`, so behaviour stays familiar.
+ try {
+ const plain = text.replace(/[*_`[\]]/g, '');
+ await client.sendMessage({ chatId, text: plain, parseMode: null });
+ return true;
+ } catch (e2: any) {
+ logError('telegramReport: mirror failed in both modes.', {
+ primary: e?.message ?? String(e),
+ fallback: e2?.message ?? String(e2),
+ });
+ return false;
+ }
+ }
+ };
+}
+
+/**
+ * Compose the per-turn Telegram message. Format mirrors Connect_origin's
+ * Secretary report so users moving between the two products see consistent
+ * notifications: header + brief + agent list + CEO synthesis + session id.
+ *
+ * The function is pure (no I/O) and exported separately so it can be unit-
+ * tested without touching VS Code APIs.
+ */
+export function formatCompanyTelegramReport(opts: {
+ state: CompanyState;
+ userPrompt: string;
+ plan: CompanyTaskPlan;
+ outputs: AgentTurnOutput[];
+ report: string;
+ sessionTimestamp: string;
+}): string {
+ const company = opts.state.companyName || '1인 기업';
+ const header = `*📱 ${company} — 작업 라운드 보고*`;
+ const cmdLine = `*명령:* ${opts.userPrompt.slice(0, 200)}`;
+ const brief = opts.plan.brief ? `\n\n*브리프:* ${opts.plan.brief}` : '';
+ const agentsLine = opts.plan.tasks.length > 0
+ ? '\n\n*완료한 에이전트:*\n' + opts.plan.tasks.map((t) => {
+ const def = COMPANY_AGENTS[t.agent];
+ const ranOk = opts.outputs.find((o) => o.agentId === t.agent && !o.error);
+ const mark = ranOk ? '✅' : '⚠️';
+ return `• ${mark} ${def?.emoji ?? ''} ${def?.name ?? t.agent}`;
+ }).join('\n')
+ : '';
+ const reportBody = opts.report.trim()
+ ? `\n\n${opts.report.trim().slice(0, REPORT_BUDGET)}`
+ : '';
+ const sessionLine = `\n\n_세션: ${opts.sessionTimestamp}_`;
+ return `${header}\n\n${cmdLine}${brief}${agentsLine}${reportBody}${sessionLine}`;
+}
diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts
index 75e6024..e2ace34 100644
--- a/src/sidebarProvider.ts
+++ b/src/sidebarProvider.ts
@@ -1276,6 +1276,11 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
context: this._context,
ai,
defaultModel: cfg.defaultModel || 'gemma4:e2b',
+ // Hand the dispatcher a thunk into ConnectAI's action-tag
+ // executor so specialist outputs like `` actually
+ // hit disk. Without this, agents would *claim* to create
+ // files while nothing happened — the exact bug we just fixed.
+ executeActionTags: (text) => this._agent.executeActionTagsOnText(text),
onEvent: emit,
});
} catch (e: any) {
@@ -1285,6 +1290,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
value: `1인 기업 모드 실행 실패: ${e?.message ?? e}`,
});
} finally {
+ // The webview's send button is locked into the "generating" state
+ // when the user submits; it only unlocks on `streamEnd`. The
+ // normal chat path posts that from inside AgentExecutor, but
+ // the company turn never touches AgentExecutor, so we have to
+ // post it ourselves here — otherwise the input stays disabled
+ // with the red Stop button after the round completes.
+ this._view?.webview.postMessage({ type: 'streamEnd' });
void this._sendReadyStatus();
}
}