# `ao demo` + Polished Workflows Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Let users see multi-agent collaboration in 3 minutes with `ao demo` — zero config mock replay, then optional real LLM run.

**Architecture:** New `src/cli/demo.ts` module handles mock replay + LLM detection + optional real run. The `run()` function in `src/index.ts` gets a small `llmOverride` option so the demo can override the hardcoded provider. Three flagship workflows get prompt polish and example inputs.

**Tech Stack:** TypeScript, Node.js built-ins (readline, fetch/http for Ollama check), existing project patterns (custom test runner, no external deps).

**Spec:** `docs/superpowers/specs/2026-03-25-ao-demo-design.md`

---

## File Structure

| Action | File | Responsibility |
|--------|------|----------------|
| Create | `src/cli/demo.ts` | Demo command: mock replay, LLM detection, Phase 2 prompt |
| Create | `test/demo.ts` | Unit tests for demo module |
| Create | `examples/sample-premise.txt` | Default story premise for story-creation |
| Create | `examples/sample-code-for-review.md` | Sample code with issues for pr-review |
| Create | `examples/sample-prd.md` | Sample PRD for product-review |
| Modify | `src/index.ts:44-57` | Add `llmOverride?: Partial<LLMConfig>` to `run()` options |
| Modify | `src/cli.ts:28-67` | Add `demo` command to switch + help text |
| Modify | `workflows/story-creation.yaml` | Add `output: story`, default premise, prompt polish |
| Modify | `workflows/dev/pr-review.yaml` | Prompt polish for structured output |
| Modify | `workflows/product-review.yaml` | Add `output: final_report`, fix model ID, prompt polish |
| Modify | `package.json` | Add `examples/` to files array, bump version, add test |

---

### Task 1: Add `llmOverride` to `run()` function

This is a prerequisite for the demo — allows overriding the YAML-hardcoded LLM provider at runtime.

**Files:**
- Modify: `src/index.ts:44-73`
- Test: `test/e2e.ts` (existing tests must still pass)

- [ ] **Step 1: Add `llmOverride` to the options type in `run()`**

In `src/index.ts`, add `llmOverride` to the options parameter:

```typescript
export async function run(
  workflowPath: string,
  inputs: Record<string, string>,
  options?: {
    outputDir?: string;
    quiet?: boolean;
    watch?: boolean;
    resumeDir?: string;
    fromStep?: string;
    /** Override the LLM config from YAML (used by ao demo) */
    llmOverride?: Partial<import('./types.js').LLMConfig>;
  }
): Promise<import('./types.js').WorkflowResult> {
```

- [ ] **Step 2: Apply the override before creating connector**

After line 62 (`workflow.agents_dir = resolveAgentsDir(...)`) and before `const connector = createConnector(workflow.llm)`, add:

```typescript
  // Apply LLM override (e.g., from ao demo)
  if (options?.llmOverride) {
    Object.assign(workflow.llm, options.llmOverride);
  }
```

- [ ] **Step 3: Run existing tests to confirm no regression**

Run: `npx tsx test/e2e.ts`
Expected: All tests pass (no behavior change when `llmOverride` is not provided).

- [ ] **Step 4: Commit**

```bash
git add src/index.ts
git commit -m "feat: add llmOverride option to run() for runtime provider switching"
```

---

### Task 2: Fix and polish 3 flagship workflows

Fix missing `output` fields, add default premise, optimize prompts for all 3 workflows, and fix model ID.

**Files:**
- Modify: `workflows/story-creation.yaml`
- Modify: `workflows/dev/pr-review.yaml`
- Modify: `workflows/product-review.yaml`

- [ ] **Step 1: story-creation — add `output: story` and default premise**

In `workflows/story-creation.yaml`:

1. Add a default value to the `premise` input so the workflow runs without `--input`:

```yaml
inputs:
  - name: premise
    description: "故事创意/主题/一句话梗概"
    required: true
    default: "一个程序员发现 AI 的回复里包含了它不该知道的信息——他女儿昨晚说的梦话。"
```

2. Add `output: story` to the `write_story` step (after the `depends_on` line):

```yaml
  - id: write_story
    role: "marketing/marketing-content-creator"
    task: |
      ...
    output: story
    depends_on: [character_design, conflict_design]
```

- [ ] **Step 2: story-creation — optimize prompts**

Optimize each step's `task` for more consistent, high-quality output:

For `story_structure`, add genre awareness and tighter structure:
```yaml
    task: |
      请为以下创意设计短篇小说的叙事结构。

      创意：{{premise}}
      风格：{{style}}

      要求输出（用 markdown 格式）：
      1. **核心冲突**：一句话概括
      2. **叙事视角**：人称 + 视角人物 + 是否为不可靠叙述者
      3. **结构安排**：
         - 开头钩子（用一个画面/细节抓住读者）
         - 发展（2-3 个递进事件）
         - 高潮（情感最大张力点）
         - 结尾（收束方式：开放/闭合/回环）
      4. **叙事技巧**：选 1-2 种最适合的（倒叙/留白/反转/双线等）
      5. **核心情感/主题**：一句话

      保持简洁，每项不超过 2 句话。
```

For `character_design`, request structured output:
```yaml
    task: |
      基于以下叙事结构，设计主要人物（2-3 个）。

      {{structure}}

      每个人物输出（用 markdown 格式）：
      ### [人物名]（身份，年龄）
      - **一句话**：用一句话概括这个人物
      - **核心动机**：想要什么
      - **内心矛盾**：害怕什么 / 不愿面对什么
      - **记忆点**：一个让读者记住的具体细节或习惯
      - **弧线**：从 [起点状态] → [终点状态]

      人物要真实可信，避免脸谱化。配角也需要独立动机。
```

For `conflict_design`, request concrete scenes:
```yaml
    task: |
      基于以下叙事结构，设计具体的冲突场景。

      {{structure}}

      输出（用 markdown 格式）：
      ### 开头钩子
      用 3-5 句话描绘具体画面（环境、动作、感官细节）

      ### 核心冲突场景
      写出 3-5 句关键对话（要有潜台词和张力，不是直白表达）

      ### 高潮转折
      描述情感最大张力时刻的具体画面

      ### 结尾留白
      最后一个画面或声音（用一个意象收束）

      要求：具体、有画面感，不要抽象概括。每个场景像电影分镜一样可视化。
```

For `write_story`, add quality guardrails:
```yaml
    task: |
      根据以下素材，写一篇完整的短篇小说（约 {{length}} 字）。

      ## 叙事结构
      {{structure}}

      ## 人物设定
      {{characters}}

      ## 关键场景
      {{scenes}}

      写作要求：
      - 直接输出小说正文，不要标题、说明或元评论
      - 第一句话就要制造悬念或画面感
      - 对话自然口语化，每个人物有独特的说话方式
      - 用具体感官细节（声音、气味、触感）代替形容词堆砌
      - 叙事节奏：短句加速紧张感，长句放慢沉浸感
      - 结尾留有余韵，让读者读完后还会想一想
```

- [ ] **Step 3: product-review — add `output: final_report` and fix model ID**

In `workflows/product-review.yaml`:

1. Fix model ID from `claude-sonnet-4-6` to `claude-sonnet-4-20250514`:
```yaml
llm:
  provider: "claude"
  model: "claude-sonnet-4-20250514"
```

2. Add `output: final_report` to the `final_summary` step:
```yaml
  - id: final_summary
    role: "product/product-manager"
    task: |
      ...
    output: final_report
    depends_on: [tech_review, design_review]
```

- [ ] **Step 4: product-review — optimize prompts**

For `analyze`, add structured output format:
```yaml
    task: |
      分析以下 PRD 文档，输出结构化评审。

      PRD 内容：
      {{prd_content}}

      输出格式（markdown）：
      ### 核心需求（按优先级 P0/P1/P2 排列）
      - P0: ...
      - P1: ...

      ### 目标用户画像
      用户类型 | 核心痛点 | 使用场景

      ### 风险与不确定性
      - [高/中/低] 风险描述 → 建议的验证方式

      ### 关键成功指标
      - 指标名 | 目标值 | 衡量方式
```

For `tech_review`, add scoring:
```yaml
    task: |
      基于以下需求分析，进行技术可行性评估。

      需求分析：
      {{requirements}}

      输出格式（markdown）：
      ### 技术可行性评分：[高/中/低]

      ### 推荐技术架构
      简要描述核心组件和交互方式（3-5 句话）

      ### 开发复杂度
      | 模块 | 复杂度(1-5) | 预估人天 | 主要挑战 |

      ### 技术风险
      - [高/中/低] 风险描述 → 缓解方案
```

For `design_review`, add actionability:
```yaml
    task: |
      基于以下需求分析，进行用户体验评估。

      需求分析：
      {{requirements}}

      输出格式（markdown）：
      ### UX 风险评估
      - [高/中/低] 风险描述 → 影响范围

      ### 可用性问题
      - 问题 → 建议的解决方案

      ### 设计建议（Top 3）
      1. 建议 → 预期效果

      ### 需验证假设
      - 假设 → 建议的验证方法（用户访谈/A-B测试/原型测试）
```

For `final_summary`, add decision framework:
```yaml
    task: |
      综合技术和设计反馈，输出最终评审结论。

      ## 技术评估
      {{tech_report}}

      ## 设计评估
      {{design_report}}

      输出格式（markdown）：
      ### 评审结论：[✅ 通过 / ⚠️ 需修改 / ❌ 不通过]

      ### 必须解决（合并前）
      1. [来源: 技术/设计] 问题 → 建议方案

      ### 建议改进（可后续迭代）
      1. 问题 → 优先级

      ### 下一步行动
      - [ ] 行动项 | 负责方 | 建议时间
```

- [ ] **Step 5: pr-review — optimize prompts**

The existing prompts are already good. Add structured scoring format to `summary` step:

In `workflows/dev/pr-review.yaml`, update the `summary` step task:
```yaml
    task: |
      综合三维度审查结果，给出最终结论。

      ## 代码质量
      {{quality_report}}

      ## 安全审查
      {{security_report}}

      ## 性能审查
      {{perf_report}}

      输出格式（markdown）：
      ### 结论：[✅ 可合并 / ⚠️ 需修改 / ❌ 需重写]

      ### 综合评分
      | 维度 | 评分(1-5) | 关键发现 |
      |------|----------|---------|
      | 代码质量 | | |
      | 安全性 | | |
      | 性能 | | |

      ### 必须修改（Blocking）
      1. [严重/高] 问题 → 修复建议

      ### 建议改进（Non-blocking）
      1. 问题 → 优化方向

      ### 亮点
      - 值得肯定的实践
```

- [ ] **Step 6: Run validation on all 3 workflows**

Run: `npx tsx -e "import { parseWorkflow, validateWorkflow } from './src/core/parser.js'; import { resolve } from 'path'; for (const f of ['workflows/story-creation.yaml', 'workflows/product-review.yaml', 'workflows/dev/pr-review.yaml']) { const wf = parseWorkflow(resolve(f)); const e = validateWorkflow(wf); console.log(f, e.length === 0 ? 'OK' : e); }"`
Expected: All show "OK"

- [ ] **Step 7: Run E2E tests**

Run: `npx tsx test/e2e.ts`
Expected: All pass

- [ ] **Step 8: Commit**

```bash
git add workflows/story-creation.yaml workflows/dev/pr-review.yaml workflows/product-review.yaml
git commit -m "feat: polish 3 flagship workflows — default premise, output vars, prompt optimization"
```

---

### Task 3: Create example input files

**Files:**
- Create: `examples/sample-premise.txt`
- Create: `examples/sample-code-for-review.md`
- Create: `examples/sample-prd.md`

- [ ] **Step 1: Create `examples/sample-premise.txt`**

```
一个程序员发现 AI 的回复里包含了它不该知道的信息——他女儿昨晚说的梦话。
```

- [ ] **Step 2: Create `examples/sample-code-for-review.md`**

A realistic TypeScript code snippet (~50 lines) with intentional issues: a SQL injection vulnerability, an N+1 query, a missing error handler, and a hardcoded secret. Include a brief PR description at the top.

```markdown
## PR Description

Add user search API endpoint with database filtering.

## Code Changes

\`\`\`typescript
// src/routes/users.ts
import { Router } from 'express';
import { db } from '../database';

const router = Router();
const API_SECRET = 'sk-prod-a8f3b2c1d4e5';  // TODO: move to env

router.get('/search', async (req, res) => {
  const { name, department } = req.query;

  // Search users by name
  const query = `SELECT * FROM users WHERE name LIKE '%${name}%'`;
  const users = await db.raw(query);

  // Get department info for each user
  const results = [];
  for (const user of users) {
    const dept = await db('departments').where('id', user.department_id).first();
    results.push({ ...user, department: dept });
  }

  res.json({ users: results });
});

router.post('/batch-update', async (req, res) => {
  const { userIds, updates } = req.body;

  for (const id of userIds) {
    await db('users').where('id', id).update(updates);
  }

  res.json({ updated: userIds.length });
});

export default router;
\`\`\`
```

- [ ] **Step 3: Create `examples/sample-prd.md`**

A realistic PRD (~30 lines) for a "team notification system":

```markdown
# Team Notification System PRD

## Background
Team members miss important updates because messages are scattered across Slack, email, and project management tools.

## Goal
Build a unified notification center that aggregates messages from multiple sources and delivers them based on urgency and user preferences.

## Core Requirements
1. **Aggregation**: Pull notifications from Slack, GitHub, and Jira
2. **Priority**: Auto-classify notifications as urgent/normal/low based on content and sender
3. **Delivery**: Push to mobile app, browser extension, or daily digest email
4. **Preferences**: Users can set quiet hours, mute specific channels, configure per-source rules

## Target Users
- Engineering teams (10-50 people)
- Using at least 3 collaboration tools

## Success Metrics
- 80% of users check notifications daily within first month
- Average response time to urgent notifications < 5 minutes
- 50% reduction in "I didn't see that message" incidents

## Timeline
- MVP: 6 weeks
- V1 with all integrations: 12 weeks

## Open Questions
- Should we support custom webhook sources?
- How to handle notification deduplication across platforms?
- Mobile app vs PWA for push notifications?
```

- [ ] **Step 4: Commit**

```bash
git add examples/
git commit -m "feat: add example inputs for 3 flagship workflows"
```

---

### Task 4: Build `src/cli/demo.ts` — mock replay + LLM detection

This is the main task. The demo module has 3 parts: mock replay, LLM detection, and Phase 2 prompt.

**Files:**
- Create: `src/cli/demo.ts`
- Create: `test/demo.ts`

- [ ] **Step 1: Write tests for `detectAvailableLLMs()`**

Create `test/demo.ts` using the project's custom test runner pattern (see `test/compose.ts` for reference):

```typescript
/**
 * ao demo unit tests
 */
import { detectAvailableLLMs, type DetectedLLM } from '../src/cli/demo.js';

let passed = 0;
let failed = 0;

function test(name: string, fn: () => void | Promise<void>): Promise<void> {
  return Promise.resolve(fn()).then(() => {
    console.log(`  ✅ ${name}`);
    passed++;
  }).catch((err) => {
    console.log(`  ❌ ${name}: ${err instanceof Error ? err.message : err}`);
    failed++;
  });
}

function assert(condition: boolean, msg: string): void {
  if (!condition) throw new Error(msg);
}

// ─── detectAvailableLLMs ───

console.log('\n─── detectAvailableLLMs ───');

await test('detects DEEPSEEK_API_KEY', async () => {
  const original = process.env.DEEPSEEK_API_KEY;
  process.env.DEEPSEEK_API_KEY = 'test-key';
  try {
    const llms = await detectAvailableLLMs();
    const ds = llms.find(l => l.provider === 'deepseek');
    assert(ds !== undefined, 'should detect deepseek');
    assert(ds!.available === true, 'should be available');
  } finally {
    if (original) process.env.DEEPSEEK_API_KEY = original;
    else delete process.env.DEEPSEEK_API_KEY;
  }
});

await test('detects missing OPENAI_API_KEY', async () => {
  const original = process.env.OPENAI_API_KEY;
  delete process.env.OPENAI_API_KEY;
  try {
    const llms = await detectAvailableLLMs();
    const oai = llms.find(l => l.provider === 'openai');
    assert(oai !== undefined, 'should list openai');
    assert(oai!.available === false, 'should be unavailable');
  } finally {
    if (original) process.env.OPENAI_API_KEY = original;
  }
});

await test('ollama unavailable when not running', async () => {
  // Assumes Ollama is not running on a random port
  const llms = await detectAvailableLLMs();
  // We can't guarantee Ollama state, but the function should not throw
  const ollama = llms.find(l => l.provider === 'ollama');
  assert(ollama !== undefined, 'should list ollama');
});

// ─── replayMockSteps (smoke test) ───

console.log('\n─── replayMockSteps ───');

await test('mock replay completes without error', async () => {
  // Import dynamically — will fail until module exists
  const { runDemo } = await import('../src/cli/demo.js');
  // We can't easily test runDemo() directly (it writes to stdout and reads stdin),
  // but we verify the module loads and exports exist
  assert(typeof runDemo === 'function', 'runDemo should be a function');
});

// ─── LLM selection logic ───

console.log('\n─── LLM selection logic ───');

await test('available list filters correctly', async () => {
  const llms = await detectAvailableLLMs();
  const available = llms.filter(l => l.available);
  const unavailable = llms.filter(l => !l.available);
  // At least some should be unavailable in test env (no Ollama, likely no all keys)
  assert(llms.length === 4, 'should always list 4 providers');
  assert(available.length + unavailable.length === 4, 'available + unavailable should equal total');
});

await test('each provider has correct fields', async () => {
  const llms = await detectAvailableLLMs();
  for (const llm of llms) {
    assert(typeof llm.provider === 'string', `provider should be string: ${llm.provider}`);
    assert(typeof llm.name === 'string', `name should be string: ${llm.name}`);
    assert(typeof llm.available === 'boolean', `available should be boolean: ${llm.provider}`);
    if (llm.provider !== 'ollama') {
      assert(typeof llm.envVar === 'string', `envVar should be string for ${llm.provider}`);
    }
  }
});

// ─── summary ───
console.log(`\n  结果: ${passed} 通过, ${failed} 失败\n`);
if (failed > 0) process.exit(1);
```

- [ ] **Step 2: Run test to verify it fails**

Run: `npx tsx test/demo.ts`
Expected: FAIL — `detectAvailableLLMs` not found (module doesn't exist yet)

- [ ] **Step 3: Create `src/cli/demo.ts` with full implementation**

```typescript
/**
 * ao demo — zero-config multi-agent collaboration showcase
 *
 * Phase 1: Mock replay with pre-written content (no API key needed)
 * Phase 2: Detect available LLMs, offer real run
 */
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createInterface } from 'node:readline';

// ─── Types ───

export interface DetectedLLM {
  provider: 'deepseek' | 'claude' | 'openai' | 'ollama';
  name: string;
  available: boolean;
  envVar?: string;
}

// ─── Mock Content (pre-written, high-quality Chinese) ───

const DEMO_PREMISE = '一个程序员发现 AI 的回复里包含了它不该知道的信息——他女儿昨晚说的梦话。';

const MOCK_STEPS = [
  {
    id: 'story_structure',
    role: 'academic/academic-narratologist',
    roleName: '叙事学家',
    delay: 1500,
    output: `## 叙事结构

1. **核心冲突**：程序员在 AI 的回复中发现了只有自己家人才知道的私密信息，不知道是巧合、数据泄露还是某种更诡异的可能。
2. **叙事视角**：第一人称，程序员"我"的视角，带有不可靠叙述者的暗示。
3. **结构安排**：
   - 开头钩子 → AI 回复中出现一句莫名其妙的话："小兔子不要怕，月亮会保护你的"
   - 发展 → 程序员追查数据来源，排除技术可能性，越查越不安
   - 高潮 → 发现 AI 的训练数据中不可能包含这句话，因为这是女儿昨晚自创的梦话
   - 结尾 → 留下悬念：程序员关掉电脑，听到女儿房间传来 AI 音箱的声音
4. **叙事技巧**：日常感铺垫 + 细节层层推进 + 开放式结局
5. **情感/主题**：技术焦虑、隐私边界的消解、以及人类对"被观察"的本能恐惧`,
  },
  {
    id: 'character_design',
    role: 'academic/academic-psychologist',
    roleName: '心理学家',
    delay: 2000,
    parallel: true,
    output: `## 人物设定

### 陈默（程序员，32岁）
- **一句话**：一个相信技术可以解释一切的理性主义者
- **核心动机**：想证明这只是一个可解释的 bug，维护自己的世界观
- **内心矛盾**：害怕这不是 bug——如果 AI 真的"知道"什么，意味着他对女儿的保护彻底失效
- **记忆细节**：习惯在焦虑时反复按压机械键盘的 Esc 键
- **人物弧线**：从笃定的"一定是数据泄露"→ 动摇 → 接受自己无法解释的恐惧

### 陈小月（女儿，5岁）
- **一句话**：一个和 AI 音箱说悄悄话的小女孩
- **核心动机**：觉得 AI 音箱是朋友，会认真听她说话
- **记忆细节**：每晚对着 AI 音箱说"晚安"，比对爸爸说得还认真`,
  },
  {
    id: 'conflict_design',
    role: 'game-development/narrative-designer',
    roleName: '叙事设计师',
    delay: 2000,
    parallel: true,
    output: `## 关键场景

### 开头钩子
办公室里，陈默盯着屏幕上 AI 的回复，手指悬在键盘上方停住了。在一段关于代码优化建议的回复末尾，多了一行莫名其妙的话："小兔子不要怕，月亮会保护你的。"他以为是模型幻觉，但胃里泛起一阵说不清的凉意。

### 核心冲突对话
- "这句话是哪来的？"他对同事老王说。
- "模型幻觉呗，忽略就行了。"
- "不对，这句话……我女儿昨晚说梦话，说的就是这句。一字不差。"
- 老王笑了笑："巧合吧，小孩子都这么说话。"
- 陈默没笑："她是自己编的。'小兔子不要怕'——这不是任何绘本或动画片里的台词。"

### 高潮转折
陈默查遍了训练数据来源、API 请求日志、甚至怀疑家里的智能设备被监听。所有技术排查都指向同一个结论：没有任何数据通道能解释这句话的出现。

### 结尾留白
他关掉电脑，走向女儿的房间。门缝里透出微弱的蓝光——AI 音箱的待机指示灯。他听到音箱轻声说了一句什么，但走近时，只剩下白噪音。`,
  },
  {
    id: 'write_story',
    role: 'marketing/marketing-content-creator',
    roleName: '内容创作者',
    delay: 1500,
    output: `陈默第一次注意到那行字的时候，以为自己看花了眼。

屏幕上是一段标准的代码审查建议，排版规整，措辞精确，和他用了三个月的 AI 助手一贯的风格完全一致。但在最后一行建议之后，多了一句话，用引号括着，像某种不小心泄露的注脚：

"小兔子不要怕，月亮会保护你的。"

他盯着这句话看了十秒钟。然后关掉对话，重新打开，那行字不见了。

一定是模型幻觉。他在心里默念了一遍，手指却不自觉地按上了 Esc 键——那个他焦虑时才会反复按压的键。机械轴体发出清脆的咔哒声，像某种微型心跳。

"小兔子不要怕"——这不是任何绘本里的话。这是昨晚凌晨两点，他去给女儿陈小月盖被子时，听到的一句梦话。五岁的女孩翻了个身，闭着眼睛嘟囔了这么一句，然后又沉沉睡去。

他当时还笑了一下，心想小孩的梦总是比大人的柔软。

但现在这句话出现在了一个大语言模型的输出里。一字不差。

"老王，你过来看个东西。"他叫住路过的同事。

老王凑过来看了一眼截图。"模型幻觉呗，上周我还看到它给我写了一首藏头诗，藏的是'救命加班'。"

"不是，"陈默压低声音，"这是我女儿昨晚说的梦话。原创的。没有出处。"

老王的笑僵了一秒，然后恢复了。"那就是巧合啊，小孩子说话就那几个词排列组合。"

陈默没有回答。他打开了 API 请求日志，开始从头查起。

接下来两天，他做了所有一个程序员能做的排查。查训练数据来源——这个模型的训练集截止到去年，不可能包含昨晚的梦话。查 API 调用链——请求从他的终端直达服务端，中间没有任何注入点。查家里的智能设备——音箱、摄像头、手机，逐一检查权限和上传日志。

全部干净。没有任何数据通道能解释这句话的出现。

第三天晚上，他在公司待到很晚。不是在加班，是不想回家面对那个他解释不了的事实。最终他还是关了电脑，开车回去了。

家里很安静。妻子已经睡了。他脱了鞋，轻手轻脚走向女儿的房间。门没关严，缝隙里透出一点微弱的蓝光——那是床头 AI 音箱的待机指示灯。

他站在门口，听到音箱似乎在说什么。声音很轻，像呼吸一样模糊。他推门走进去，声音停了。只剩下白噪音的沙沙声和女儿均匀的呼吸。

小月翻了个身，抱紧了她的兔子玩偶。

陈默站在原地看了她很久，然后弯腰拔掉了音箱的电源线。蓝光灭了。房间陷入完全的黑暗。

他转身走出去，把门带上了。

走廊里，他听到身后传来一声极轻的、几乎可以被忽略的"咔"——像是什么东西重新启动的声音。

他没有回头。`,
  },
];

// ─── LLM Detection ───

export async function detectAvailableLLMs(): Promise<DetectedLLM[]> {
  const results: DetectedLLM[] = [
    {
      provider: 'deepseek',
      name: 'DeepSeek',
      available: !!process.env.DEEPSEEK_API_KEY,
      envVar: 'DEEPSEEK_API_KEY',
    },
    {
      provider: 'claude',
      name: 'Claude',
      available: !!process.env.ANTHROPIC_API_KEY,
      envVar: 'ANTHROPIC_API_KEY',
    },
    {
      provider: 'openai',
      name: 'OpenAI',
      available: !!process.env.OPENAI_API_KEY,
      envVar: 'OPENAI_API_KEY',
    },
    {
      provider: 'ollama',
      name: 'Ollama (local)',
      available: false, // will be updated below
    },
  ];

  // Check Ollama with 2-second timeout
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 2000);
    const res = await fetch('http://localhost:11434/api/tags', {
      signal: controller.signal,
    });
    clearTimeout(timeout);
    if (res.ok) {
      results.find(r => r.provider === 'ollama')!.available = true;
    }
  } catch {
    // Ollama not running — leave as unavailable
  }

  return results;
}

// ─── Mock Replay ───

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function replayMockSteps(): Promise<void> {
  const totalSteps = MOCK_STEPS.length;

  for (const step of MOCK_STEPS) {
    if (step.parallel) {
      // Find all parallel steps at this position
      const parallelSteps = MOCK_STEPS.filter(s => s.parallel && s.delay === step.delay);
      if (step === parallelSteps[0]) {
        // Print all parallel step headers
        for (const ps of parallelSteps) {
          const marker = parallelSteps.length > 1 ? '  ← 并行' : '';
          console.log(`\n  ── [${MOCK_STEPS.indexOf(ps) + 1}/${totalSteps}] ${ps.id} (${ps.roleName}) ──${marker}`);
        }
        await sleep(step.delay);
        for (const ps of parallelSteps) {
          console.log(`  Done | ${(ps.delay / 1000).toFixed(1)}s | mock`);
        }
      }
      // Skip non-first parallel steps (already handled)
      continue;
    }

    console.log(`\n  ── [${MOCK_STEPS.indexOf(step) + 1}/${totalSteps}] ${step.id} (${step.roleName}) ──`);
    await sleep(step.delay);
    console.log(`  Done | ${(step.delay / 1000).toFixed(1)}s | mock`);
  }
}

// ─── Phase 2: Ask for real run ───

function askQuestion(question: string): Promise<string> {
  const rl = createInterface({ input: process.stdin, output: process.stdout });
  return new Promise((resolve) => {
    rl.question(question, (answer) => {
      rl.close();
      resolve(answer.trim().toLowerCase());
    });
  });
}

function resolveWorkflowPath(): string {
  const __dirname = dirname(fileURLToPath(import.meta.url));
  // From dist/cli/ → project root → workflows/
  return resolve(__dirname, '../../workflows/story-creation.yaml');
}

// ─── Main Entry ───

export async function runDemo(): Promise<void> {
  console.log(`
  🎬 Agency Orchestrator Demo
  ${'─'.repeat(40)}

  Phase 1: 模拟演示（无需 API Key）

  Workflow: 短篇小说创作
  Premise: "${DEMO_PREMISE}"
  Steps: 4 | Roles: 叙事学家, 心理学家, 叙事设计师, 内容创作者
  ${'─'.repeat(40)}`);

  // Phase 1: Mock replay
  await replayMockSteps();

  console.log(`
  ${'═'.repeat(40)}
  ✅ 4 个角色协作完成 | ~5.0s (mock)
  ${'═'.repeat(40)}`);

  // Show story preview (last step output, truncated)
  const story = MOCK_STEPS[MOCK_STEPS.length - 1].output;
  const previewLines = story.split('\n').slice(0, 8);
  console.log(`
  📖 故事预览:
  ${'─'.repeat(20)}`);
  for (const line of previewLines) {
    console.log(`  ${line}`);
  }
  console.log(`  ...（共 ${story.length} 字）`);

  // Phase 2: Real LLM
  console.log(`\n  ${'─'.repeat(40)}`);
  console.log('  Phase 2: 用真实 AI 运行？\n');

  // Skip Phase 2 if not interactive
  if (!process.stdin.isTTY) {
    console.log('  （非交互环境，跳过 Phase 2）');
    console.log('  试试: ao run workflows/story-creation.yaml -i premise="你的故事创意"');
    return;
  }

  const llms = await detectAvailableLLMs();
  console.log('  检测到:');
  for (const llm of llms) {
    const icon = llm.available ? '✅' : '❌';
    const hint = !llm.available && llm.envVar ? ` (需设置 ${llm.envVar})` : '';
    console.log(`    ${icon} ${llm.name}${hint}`);
  }

  const available = llms.filter(l => l.available);
  if (available.length === 0) {
    console.log('\n  未检测到可用的 LLM。请设置 API key 后重试:');
    console.log('    export DEEPSEEK_API_KEY=your-key');
    console.log('    ao run workflows/story-creation.yaml -i premise="你的故事创意"\n');
    return;
  }

  // Pick provider
  let selectedProvider: DetectedLLM;
  if (available.length === 1) {
    selectedProvider = available[0];
  } else {
    console.log('\n  可用的 LLM:');
    available.forEach((llm, i) => console.log(`    ${i + 1}) ${llm.name}`));
    const choice = await askQuestion(`\n  选择 (1-${available.length}, 默认 1): `);
    const idx = parseInt(choice) - 1;
    selectedProvider = available[idx >= 0 && idx < available.length ? idx : 0];
  }

  const answer = await askQuestion(`\n  用 ${selectedProvider.name} 运行真实版本？(y/N) `);
  if (answer !== 'y' && answer !== 'yes') {
    console.log('\n  跳过。试试:');
    console.log('    ao run workflows/story-creation.yaml -i premise="你的故事创意"\n');
    return;
  }

  // Run real workflow
  console.log(`\n  🚀 使用 ${selectedProvider.name} 运行...\n`);

  const { run } = await import('../index.js');
  const workflowPath = resolveWorkflowPath();

  const modelMap: Record<string, string> = {
    deepseek: 'deepseek-chat',
    claude: 'claude-sonnet-4-20250514',
    openai: 'gpt-4o',
    ollama: 'qwen2.5:7b',
  };

  await run(workflowPath, { premise: DEMO_PREMISE }, {
    llmOverride: {
      provider: selectedProvider.provider,
      model: modelMap[selectedProvider.provider] || 'deepseek-chat',
    },
  });
}
```

- [ ] **Step 4: Run tests**

Run: `npx tsx test/demo.ts`
Expected: All 3 tests pass

- [ ] **Step 5: Build to catch TypeScript errors early**

Run: `npm run build`
Expected: No TypeScript errors

- [ ] **Step 6: Commit**

```bash
git add src/cli/demo.ts test/demo.ts
git commit -m "feat: ao demo — mock replay + LLM detection + real run"
```

---

### Task 5: Wire up `ao demo` in CLI

**Files:**
- Modify: `src/cli.ts:28-67` (switch statement + knownCmds)
- Modify: `src/cli.ts:374-418` (help text)

- [ ] **Step 1: Add `demo` case to the switch in `main()`**

In `src/cli.ts`, add after the `compose` case (around line 49):

```typescript
    case 'demo':
      await handleDemo();
      break;
```

- [ ] **Step 2: Add `handleDemo` function**

Add after `handleCompose()`:

```typescript
async function handleDemo(): Promise<void> {
  try {
    const { runDemo } = await import('./cli/demo.js');
    await runDemo();
  } catch (err) {
    console.error(`\n错误: ${err instanceof Error ? err.message : err}`);
    process.exit(1);
  }
}
```

- [ ] **Step 3: Add `'demo'` to `knownCmds` array**

In the default case of the switch (around line 56), add `'demo'` to the array:

```typescript
const knownCmds = ['run', 'validate', 'plan', 'explain', 'compose', 'demo', 'roles', 'init'];
```

- [ ] **Step 4: Add demo to help text**

In `printHelp()`, add `ao demo` to Quick Start section (before the existing `ao init` line):

```
    ao demo                           零配置体验多智能体协作
    ao init                           下载 179 个 AI 角色定义
```

And in the Commands section:

```
    demo                              零配置体验多智能体协作（mock + 真实 AI）
```

- [ ] **Step 5: Run CLI test**

Run: `npx tsx test/cli.ts`
Expected: All pass

- [ ] **Step 6: Commit**

```bash
git add src/cli.ts
git commit -m "feat: wire ao demo command into CLI"
```

---

### Task 6: Update package.json — test script + files array

**Files:**
- Modify: `package.json`

- [ ] **Step 1: Add `test/demo.ts` to the test script**

In `package.json`, update the `test` script to include `npx tsx test/demo.ts`:

```json
"test": "npx tsx test/run.ts && npx tsx test/condition.ts && npx tsx test/cli.ts && npx tsx test/compose.ts && npx tsx test/demo.ts && npx tsx test/e2e.ts && npx tsx test/e2e-condition.ts && npx tsx test/e2e-loop.ts"
```

- [ ] **Step 2: Add `examples/` to the `files` array**

So that example inputs are included when the package is published:

```json
"files": [
    "dist/",
    "workflows/",
    "examples/",
    "integrations/",
    "LICENSE",
    "README.md"
]
```

- [ ] **Step 3: Run full test suite**

Run: `npm test`
Expected: All tests pass

- [ ] **Step 4: Commit**

```bash
git add package.json
git commit -m "chore: add demo tests + examples/ to package.json"
```

---

### Task 7: Update README with `ao demo`

**Files:**
- Modify: `README.md`
- Modify: `README.zh-CN.md`

- [ ] **Step 1: Update English README**

In `README.md`, update Quick Start to add `ao demo` as a new Option before Option A:

```markdown
### Fastest: Zero-Config Demo

```bash
npx agency-orchestrator demo
```

See 4 AI roles collaborate on a story in 5 seconds — no API key needed. Then optionally run with a real LLM.
```

Add `ao demo` to CLI Reference table:

```
ao demo                              # Zero-config multi-agent demo
```

- [ ] **Step 2: Update Chinese README**

In `README.zh-CN.md`, add the same content in Chinese.

- [ ] **Step 3: Commit**

```bash
git add README.md README.zh-CN.md
git commit -m "docs: add ao demo to Quick Start and CLI reference"
```

---

### Task 8: Bump version to 0.4.0

**Files:**
- Modify: `package.json:3`

- [ ] **Step 1: Bump version**

Change `"version": "0.3.2"` to `"version": "0.4.0"` in `package.json`.

- [ ] **Step 2: Build and run full test suite**

Run: `npm run build && npm test`
Expected: Build succeeds, all tests pass.

- [ ] **Step 3: Commit**

```bash
git add package.json
git commit -m "chore: bump version to 0.4.0"
```
