> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Plan 01 — callytics-common Schema 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:** Add the Phase 1 schema changes to `callytics-common`(new `task_progress_events` table, new `tasks` columns/index/CHECKs, updated enums, drizzle migration files), shipping `@retaintive/common` 跨过渡期 + final state 双兼容。

**Architecture:** 改 5 个文件 — `src/db/schema/tasks.ts`(enum + column 改)、`src/db/schema/task-progress-events.ts`(新建)、`src/db/schema/task-ui.ts`(UI const 改)、`src/db/schema/index.ts`(export)、`drizzle/0010_phase1_transitional.sql`(新 migration)。同时加 unit test 验证 enum + Drizzle table 定义。**先 ship transitional 状态**(三值 status enum + 旧字段保留),让 caller 在过渡期内能继续工作;final cutover 由 Plan 07 单独执行。

**Tech Stack:** TypeScript 5 / Drizzle ORM / drizzle-kit 0.20+ / bun test / PostgreSQL 16(Neon)/ `@retaintive/common` npm package

**Spec source:** `docs/product-design/v2/unified-pipeline/implementation-plan/normative-spec.md` §2 + §6.2 transitional schema

***

## File Structure

| File                                        | Purpose                                                    | Action                                                                                           |
| ------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `src/db/schema/tasks.ts`                    | tasks 表 + 6 个 enum const + CHECK + index                   | Modify(加 3 enum / 加 3 column / 改 status enum 三值 / 加 partial unique)                              |
| `src/db/schema/task-progress-events.ts`     | 新 task\_progress\_events 表 + 3 个新 enum                     | **Create**                                                                                       |
| `src/db/schema/contact-timeline.ts`         | 加 `task.progress_recorded` 进 TIMELINE\_EVENT\_TYPES        | Modify(Plan 06 §timeline event emit 依赖,Codex audit 抓的 missing)                                   |
| `src/db/schema/task-ui.ts`                  | UI label const(CLOSE\_RESULT\_OPTIONS / STATUS\_OPTIONS 等) | Modify(加 `'open'` STATUS\_OPTIONS value / 加 PROGRESS\_OPTIONS / closeResult 加 unable\_to\_reach) |
| `src/db/schema/index.ts`                    | barrel export                                              | Modify(加 task-progress-events export)                                                            |
| `drizzle/0010_phase1_transitional.sql`      | migration SQL                                              | **Create**                                                                                       |
| `tests/schema/task-progress-events.test.ts` | unit test for new table + enums                            | **Create**                                                                                       |
| `tests/schema/tasks-phase1.test.ts`         | unit test for tasks 改动                                     | **Create**                                                                                       |

***

## Task 1: Add `task_progress_events` Drizzle table file

**Files:**

- Create: `src/db/schema/task-progress-events.ts`
- Create: `tests/schema/task-progress-events.test.ts` (unit — 纯 TS,不连 DB)
- Create: `tests/integration/task-progress-events.test.ts` (integration — 连 test Neon)

### Step 1: Map test scenarios (before writing any test code)

**原则**:schema 是 declarative,读 file 就能 verify 大部分东西。**不 test framework / TS 编译保证 / DB 自带约束**。只 test 真 invariant —— 违反会出 bug 那些。

| # | Scenario                                                        | 类型          | 为什么 test 它                                                                                 |
| - | --------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
| 1 | 3 个 enum 值 + 顺序 exactly match normative-spec §2.1               | Unit        | 防 spec drift —— 改 spec 时这条 test 会逼着同步改代码                                                   |
| 2 | `taskProgressEvents` 有 15 个 expected column 名                   | Unit        | 防漏 column / typo column 名                                                                  |
| 3 | INSERT 完整合法 row → SELECT 可拿回                                    | Integration | 验 schema 能用 + Drizzle table def 跟实际 SQL 匹配                                                 |
| 4 | INSERT duplicate `idempotency_key` → fail with UNIQUE violation | Integration | **关键 invariant** —— `idempotency_key` 是 record\_progress 唯一去重机制,违反 = attempt\_count 翻倍 bug |
| 5 | INSERT `task_id` 指向不存在 tasks 行 → fail with FK violation         | Integration | 验 FK 真生效(Drizzle `references()` 不一定都 emit FK,要 verify)                                     |

**不 test 的事**(明确列出来让 implementer 知道为啥不写):

- Enum `as const` immutable / TS narrowing — TypeScript 编译时强制
- column nullability / DEFAULT NOW / index 存在 — read schema file 看就行,test 边际价值低
- NOT NULL violation on 各 column — DB 自带约束,test PostgreSQL 本身没意义
- enum CHECK violation —— Drizzle 用 text + enum hint,不是 PG enum type,**PG 不强制**,test 这条会假阳性

### Step 2: Write tests (sample style — implementer 自己完成所有 case)

写 unit test 用 `bun test` BDD 风格。Sample(U1 — 让 implementer 知道 import + assertion 形态):

```typescript
// tests/schema/task-progress-events.test.ts
import { describe, expect, test } from 'bun:test';
import {
  taskProgressEvents,
  TASK_PROGRESS_TYPE,
  TASK_CHANNEL,
  TASK_ACTOR_TYPE,
} from '../../src/db/schema/task-progress-events';

describe('task-progress-events schema — unit', () => {
  test('U1: TASK_PROGRESS_TYPE matches spec §2.1 exact order', () => {
    expect(TASK_PROGRESS_TYPE).toEqual([
      'no_answer',
      'left_voicemail',
      'text_sent',
      'callback_requested',
      'follow_up_scheduled',
      'customer_considering',
    ]);
  });

  // U2-U14: implementer 按上方 case map 全 cover
  // 整 file 估计 ~150 行 test code
});
```

Integration test 文件 sample(I1 — 让 implementer 知道连 DB pattern):

```typescript
// tests/integration/task-progress-events.test.ts
import { describe, expect, test, beforeAll, afterAll } from 'bun:test';
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import { taskProgressEvents } from '../../src/db/schema/task-progress-events';
import { tasks } from '../../src/db/schema/tasks';

const sql = neon(process.env['DATABASE_URL_TEST']!);
const db = drizzle(sql);

describe('task-progress-events — DB integration', () => {
  let seedTaskId: string;
  beforeAll(async () => {
    // seed 1 个 task 给 FK reference 用
    const [task] = await db.insert(tasks).values({
      contactPhone: '+15555550001',
      franchiseId: 'TEST',
      accountId: 'TEST',
      storeId: 'STORE_TEST',
      taskType: 'follow_up',
      typeCategory: 'lead_follow_up',
      sourceType: 'manual',
      status: 'open',
    }).returning();
    seedTaskId = task!.taskId;
  });

  afterAll(async () => {
    // cleanup
    await db.delete(taskProgressEvents).where(/* by test storeId */);
    await db.delete(tasks).where(/* same */);
  });

  test('I1: INSERT 完整合法 row succeeds and is retrievable', async () => {
    const [row] = await db.insert(taskProgressEvents).values({
      taskId: seedTaskId,
      storeId: 'STORE_TEST',
      contactPhone: '+15555550001',
      progressType: 'no_answer',
      channel: 'phone',
      actorType: 'staff',
      callId: 'call_abc',
      occurredAt: new Date(),
      idempotencyKey: `progress:${seedTaskId}:call:call_abc:no_answer`,
    }).returning();
    expect(row).toBeDefined();
    expect(row?.taskId).toBe(seedTaskId);
  });

  // I2-I14: implementer 按 case map 全 cover
  // 整 file 估计 ~250 行 test code
});
```

### Step 3: Run tests to verify they fail

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test tests/schema/task-progress-events.test.ts
```

Expected: FAIL with "Cannot find module '../../src/db/schema/task-progress-events'"

Integration tests will additionally fail to import schema. **暂不跑 integration test** — 等 Task 5 migration apply 后再跑。

- [ ] **Step 3: Implement the schema file**

Create `src/db/schema/task-progress-events.ts`:

```typescript
import {
  pgTable,
  text,
  timestamp,
  uuid,
  bigint,
  index,
  uniqueIndex,
  check,
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';

// ─── Enum definitions (single source of truth) ──────────────────

export const TASK_PROGRESS_TYPE = [
  'no_answer',
  'left_voicemail',
  'text_sent',
  'callback_requested',
  'follow_up_scheduled',
  'customer_considering',
] as const;
export type TaskProgressType = (typeof TASK_PROGRESS_TYPE)[number];

export const TASK_CHANNEL = ['phone', 'sms', 'voicemail', 'email'] as const;
export type TaskChannel = (typeof TASK_CHANNEL)[number];

export const TASK_ACTOR_TYPE = ['staff', 'system', 'ai_agent', 'contact_analysis'] as const;
export type TaskActorType = (typeof TASK_ACTOR_TYPE)[number];

// ─── Table definition ───────────────────────────────────────────

/**
 * task_progress_events — Progress 事件 source of truth
 *
 * 每次员工 / 系统 / AI 对某个 open task 做了一次"进展"动作,就 INSERT 一行。
 * 不 close task。例如:打电话没接 → no_answer;留语音邮件 → left_voicemail;
 * 发短信 → text_sent。task 仍 open,attemptCount += 1。
 *
 * 替代旧设计:closeResult 里混合的 'no_answer' / 'left_voicemail' / 'callback_later'
 * / 'attempted' 这 4 个 "其实是 progress,不是 outcome" 的值。
 *
 * 多租户隔离:store_id 冗余存储,避免 join tasks 查 store guard。
 *
 * 唯一去重键:idempotency_key UNIQUE。不用 (task_id, call_id, progress_type)
 * 复合 unique 因为 PostgreSQL NULL ≠ NULL —— SMS progress (call_id IS NULL)
 * 会被重复插入。caller 生成 idempotency_key 见 normative-spec §3.2。
 *
 * 设计文档:docs/product-design/v2/unified-pipeline/implementation-plan/normative-spec.md §2.1
 */
export const taskProgressEvents = pgTable(
  'task_progress_events',
  {
    eventId: uuid('event_id').primaryKey().defaultRandom(),
    /**
     * FK → tasks.task_id(normative-spec §2.1 line 39 明文要求)。
     * 没 FK 会导致 orphan event row(task 被 hard-delete 时 event 残留)。
     * Phase 1 不做 cascade delete(spec 没要求),默认 NO ACTION 让 DB 阻止
     * task 删除直到 event 被 archive。
     */
    taskId: uuid('task_id').notNull().references(() => tasks.taskId),
    storeId: text('store_id').notNull(),
    contactPhone: text('contact_phone').notNull(),
    progressType: text('progress_type', { enum: TASK_PROGRESS_TYPE }).notNull(),
    channel: text('channel', { enum: TASK_CHANNEL }).notNull(),
    actorType: text('actor_type', { enum: TASK_ACTOR_TYPE }).notNull(),
    actorId: uuid('actor_id'),
    /**
     * evidence reference,no FK。
     * calls 表 PK 是 telephony_session_id(text)而非 uuid;且 call 可能在 progress
     * 写入之后才落库,加 FK 会引入时序耦合。Phase 1 不加 FK,只作 reference。
     */
    callId: text('call_id'),
    /**
     * evidence reference,no FK。
     * messages.id 是 bigint。同理不加 FK 避免时序耦合。
     */
    messageId: bigint('message_id', { mode: 'bigint' }),
    note: text('note'),
    nextDueAt: timestamp('next_due_at', { withTimezone: true }),
    occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull(),
    /**
     * NOT NULL UNIQUE —— 全局唯一去重键。
     * 生成规则见 normative-spec §3.2:
     *   电话:`progress:{taskId}:call:{callId}:{progressType}`
     *   SMS:`progress:{taskId}:message:{messageId}:{progressType}`
     *   手动:`progress:{taskId}:manual:{actorId}:{occurredAt}:{progressType}`
     *   系统:`progress:{taskId}:system:{runId}:{progressType}`
     */
    idempotencyKey: text('idempotency_key').notNull().unique(),
    createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  },
  (table) => [
    // task 维度查询(API 拉某 task 的全部进展)
    index('idx_tpe_task_occurred').on(table.taskId, table.occurredAt.desc()),
    // store 维度查询(dashboard "attempt workload")
    index('idx_tpe_store_occurred').on(table.storeId, table.occurredAt.desc()),
    // actor 维度查询(staff 个人产出统计)
    index('idx_tpe_actor').on(table.actorType, table.actorId, table.occurredAt.desc()),
    // progress 类型 + channel 统计
    index('idx_tpe_type_channel').on(table.progressType, table.channel, table.occurredAt.desc()),
    /*
     * DB-level CHECK constraints — normative-spec §2.1 line 42-47 明文要求。
     * Drizzle text+enum 只 TS 层验 — 任何 SQL 直插(staff ad-hoc / migration /
     * 旧 caller 没升级 client) 会绕过 TS 检查。CHECK 在 DB 层守 schema 不变量。
     */
    check('chk_tpe_progress_type', sql`progress_type IN (${sql.raw(TASK_PROGRESS_TYPE.map(v => `'${v}'`).join(', '))})`),
    check('chk_tpe_channel', sql`channel IN (${sql.raw(TASK_CHANNEL.map(v => `'${v}'`).join(', '))})`),
    check('chk_tpe_actor_type', sql`actor_type IN (${sql.raw(TASK_ACTOR_TYPE.map(v => `'${v}'`).join(', '))})`),
  ]
);
```

- [ ] **Step 4: Run test to verify it passes**

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test tests/schema/task-progress-events.test.ts
```

Expected: PASS — 4 tests pass

- [ ] **Step 5: Commit**

```bash
cd /Users/maxwsy/workspace/callytics-common
git add src/db/schema/task-progress-events.ts tests/schema/task-progress-events.test.ts
git commit -m "feat(schema): add task_progress_events table + 3 new enums

Phase 1 Plan 01 Task 1.

新表替代旧设计里 closeResult 混合的 4 个 progress 值
(no_answer / left_voicemail / callback_later / attempted)。
task 不再因为 progress 被 close;改成 INSERT 一行 progress event +
tasks.attempt_count++。

Spec: docs/product-design/v2/unified-pipeline/implementation-plan/normative-spec.md §2.1
"
```

***

## Task 2: Update `src/db/schema/tasks.ts` — transitional `TASK_STATUS` enum + new columns

**Files:**

- Modify: `src/db/schema/tasks.ts:16` (TASK\_STATUS), `:32` (TASK\_CLOSE\_TYPE), `:38-54` (TASK\_CLOSE\_RESULT), `:90-309` (table definition)

- Test: `tests/schema/tasks-phase1.test.ts`

- [ ] **Step 1: Write the failing test**

Create `tests/schema/tasks-phase1.test.ts`:

```typescript
import { describe, expect, test } from 'bun:test';
import {
  TASK_STATUS,
  TASK_CLOSE_TYPE,
  TASK_CLOSE_RESULT,
  tasks,
} from '../../src/db/schema/tasks';

describe('tasks schema — Phase 1 transitional', () => {
  test('TASK_STATUS has 3 values during transitional window: pending, open, closed', () => {
    expect(TASK_STATUS).toEqual(['pending', 'open', 'closed']);
  });

  test('TASK_CLOSE_TYPE adds create_closed (3 values total)', () => {
    expect(TASK_CLOSE_TYPE).toEqual(['auto_closed', 'manual_closed', 'create_closed']);
  });

  test('TASK_CLOSE_RESULT adds unable_to_reach (19 values total during transitional)', () => {
    expect(TASK_CLOSE_RESULT).toContain('unable_to_reach');
    // transitional: old 18 + new unable_to_reach = 19; final cutover (Plan 07) shrinks to 15
    expect(TASK_CLOSE_RESULT.length).toBe(19);
  });

  test('tasks table exposes new columns: executorType, attemptCount, sourceCallId', () => {
    const columns = Object.keys(tasks);
    expect(columns).toContain('executorType');
    expect(columns).toContain('attemptCount');
    expect(columns).toContain('sourceCallId');
  });
});
```

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

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test tests/schema/tasks-phase1.test.ts
```

Expected: FAIL — `TASK_STATUS` does not include 'open';`TASK_CLOSE_TYPE` length is 2;`unable_to_reach` not in `TASK_CLOSE_RESULT`;`tasks.executorType` undefined

- [ ] **Step 3: Update `TASK_STATUS` enum**

Edit `src/db/schema/tasks.ts` line 16:

```typescript
// BEFORE:
export const TASK_STATUS = ['pending', 'closed'] as const;

// AFTER:
/**
 * Phase 1 transitional: 3 values during compatibility window.
 * Final cutover (Plan 07) shrinks back to ['open', 'closed'].
 *   - 'pending' — legacy value, kept readable during transition
 *   - 'open'    — new canonical value, all new writes use this
 *   - 'closed'  — terminal state (unchanged)
 * Read paths must use status IN ('pending', 'open') for "open work" queries
 * until cutover migration runs (UPDATE pending → open + DROP from enum).
 * Spec: normative-spec.md §6.2
 */
export const TASK_STATUS = ['pending', 'open', 'closed'] as const;
```

- [ ] **Step 4: Update `TASK_CLOSE_TYPE` enum**

Edit `src/db/schema/tasks.ts` line 32:

```typescript
// BEFORE:
export const TASK_CLOSE_TYPE = ['auto_closed', 'manual_closed'] as const;

// AFTER:
/**
 * Phase 1: add 'create_closed' for tasks created already-closed
 * (e.g. inbound 当场成交 → 直接建 closed task,让 dashboard 能看到完成事项)。
 * Spec: normative-spec.md §2.2 + unified-pipeline-final.md Task Pipeline Deliverable
 */
export const TASK_CLOSE_TYPE = ['auto_closed', 'manual_closed', 'create_closed'] as const;
```

- [ ] **Step 5: Update `TASK_CLOSE_RESULT` enum**

Edit `src/db/schema/tasks.ts` lines 38-54. Add `'unable_to_reach'` to the array:

```typescript
// BEFORE: 18 values
export const TASK_CLOSE_RESULT = [
  'attempted', 'converted', 'win_back', 'issue_resolved',
  'cancel_saved', 'renewed', 'upgraded', 'referral_obtained',
  'wrong_number', 'do_not_contact', 'other',
  'no_answer', 'left_voicemail', 'not_interested', 'callback_later', 'already_member',
  /* ... existing comment about booked/cancelled ... */
  'booked', 'cancelled',
] as const;

// AFTER: 19 values transitional (final cutover shrinks to 15 by removing 4 progress values)
export const TASK_CLOSE_RESULT = [
  'attempted', 'converted', 'win_back', 'issue_resolved',
  'cancel_saved', 'renewed', 'upgraded', 'referral_obtained',
  'wrong_number', 'do_not_contact', 'other',
  'no_answer', 'left_voicemail', 'not_interested', 'callback_later', 'already_member',
  /*
   * Added 2026-05-23 for OTF V1 prompt refinements (callytics-infrastructure
   * PR #980 series). Distinguish customer-facing outcomes the AI must record
   * on task close that the prior 16 values couldn't express cleanly:
   *   - `booked`: lead has a confirmed upcoming appointment/class/intro. Not
   *     the same as `converted` (paid membership) — booked may still no-show.
   *   - `cancelled`: cancellation request approved OR cancellation form sent
   *     to customer. Not the same as `cancel_saved` (customer was talked out
   *     of cancelling and is staying).
   */
  'booked', 'cancelled',
  /*
   * Added 2026-06 Phase 1 (normative-spec §2.2): represents "max attempts
   * reached, contact could not be reached" — the only legitimate "negative
   * outcome" after Phase 1 moves no_answer / left_voicemail / attempted into
   * task_progress_events. Final cutover (Plan 07) removes the 4 progress
   * values, leaving 15 outcome-only values.
   */
  'unable_to_reach',
] as const;
```

- [ ] **Step 6: Add new columns to tasks pgTable**

Edit `src/db/schema/tasks.ts` — inside `pgTable('tasks', { ... })` add 3 new columns. Find the section after `closeNote` / `note` (around line 213) and before `createdAt` (line 217):

```typescript
    /* existing columns up to note ... */
    note: text('note'),

    // ── Phase 1 新增字段(normative-spec §2.2)──

    /**
     * 执行 / 完成者类型,metrics 归因用。
     * 'human'     — staff 通过 UI 操作
     * 'ai_agent'  — future tool calling AI agent
     * 'system'    — pipeline 自动(e.g. DNC cascade)
     * Phase 1 大多数 task 是 human / system,ai_agent 留给 future。
     */
    executorType: text('executor_type', {
      enum: ['human', 'ai_agent', 'system'] as const,
    }),
    // CHECK 见 Step 7(connected to existing chk_tasks_* checks block)

    /**
     * 列表展示用 snapshot,真实 source of truth 是 task_progress_events 表。
     * Orchestrator 在 record_progress action 内用 CTE 保证只在真插入 progress event 时
     * 才 increment(normative-spec §3.2 B8 fix)。
     */
    attemptCount: integer('attempt_count').notNull().default(0),

    /**
     * create_closed task 的 evidence reference,对应触发"当场办成"的 call ID。
     * 用于 partial unique 防同一通电话重复建 closed task(normative-spec §3.1)。
     * 普通 create_open / staff manual close 时为 NULL。
     */
    sourceCallId: text('source_call_id'),

    // ── 系统时间戳 ──

    createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
    /* ... rest unchanged ... */
```

**Important:** Also add `integer` to the import at the top of the file:

```typescript
// BEFORE:
import {
  pgTable, text, timestamp, uuid, boolean, jsonb,
  index, uniqueIndex, check,
} from 'drizzle-orm/pg-core';

// AFTER:
import {
  pgTable, text, timestamp, uuid, boolean, jsonb, integer,
  index, uniqueIndex, check,
} from 'drizzle-orm/pg-core';
```

- [ ] **Step 7: Update CHECK constraints — transitional window**

Edit `src/db/schema/tasks.ts` line 262 — replace `chk_tasks_closed_integrity`:

```typescript
// BEFORE:
check('chk_tasks_closed_integrity', sql`
  (status = 'pending' AND close_type IS NULL AND closed_at IS NULL)
  OR
  (status = 'closed' AND close_type IS NOT NULL AND closed_at IS NOT NULL)
`),

// AFTER (transitional — accepts both 'pending' and 'open' as open-work state):
check('chk_tasks_closed_integrity', sql`
  (status IN ('pending', 'open') AND close_type IS NULL AND closed_at IS NULL)
  OR
  (status = 'closed' AND close_type IS NOT NULL AND closed_at IS NOT NULL)
`),
```

Edit `src/db/schema/tasks.ts` line 276 — update `chk_tasks_close_result` to include `'unable_to_reach'`:

```typescript
// BEFORE:
check('chk_tasks_close_result', sql`
  close_result IS NULL OR close_result = ANY(ARRAY[
    'attempted', 'converted', 'win_back', 'issue_resolved',
    'cancel_saved', 'renewed', 'upgraded', 'referral_obtained',
    'wrong_number', 'do_not_contact', 'other',
    'no_answer', 'left_voicemail', 'not_interested', 'callback_later', 'already_member',
    'booked', 'cancelled'
  ])
`),

// AFTER (adds 'unable_to_reach'):
check('chk_tasks_close_result', sql`
  close_result IS NULL OR close_result = ANY(ARRAY[
    'attempted', 'converted', 'win_back', 'issue_resolved',
    'cancel_saved', 'renewed', 'upgraded', 'referral_obtained',
    'wrong_number', 'do_not_contact', 'other',
    'no_answer', 'left_voicemail', 'not_interested', 'callback_later', 'already_member',
    'booked', 'cancelled',
    'unable_to_reach'
  ])
`),
```

- [ ] **Step 8: Update partial unique indexes — transitional window**

Edit `src/db/schema/tasks.ts` line 254 — `uq_tasks_pending_contact_category` WHERE clause:

```typescript
// BEFORE:
uniqueIndex('uq_tasks_pending_contact_category')
  .on(table.contactPhone, table.storeId, table.typeCategory)
  .where(sql`status = 'pending' AND store_id IS NOT NULL`),

// AFTER (transitional — covers both 'pending' and 'open' open-state names):
uniqueIndex('uq_tasks_pending_contact_category')
  .on(table.contactPhone, table.storeId, table.typeCategory)
  .where(sql`status IN ('pending', 'open') AND store_id IS NOT NULL`),
```

Add NEW partial unique for create\_closed dedup. Insert after `uq_tasks_pending_contact_category`:

```typescript
// #8b create_closed 防重 — 同 (contactPhone, storeId, typeCategory, sourceCallId)
// 在 closed 状态下只允许 1 行。sourceCallId IS NOT NULL 排除普通 manual close。
// 防止同一通电话被同一个 caller 重复建 closed task。
uniqueIndex('uq_tasks_create_closed_evidence')
  .on(table.contactPhone, table.storeId, table.typeCategory, table.sourceCallId)
  .where(sql`status = 'closed' AND source_call_id IS NOT NULL`),
```

- [ ] **Step 8c: Add DB-level CHECK for executor\_type**

normative-spec §2.2 line 89-91 明文要求 DB CHECK,**不能只靠 Drizzle text+enum hint**(那只是 TS 检查,SQL 直插 / 旧 client 都绕过)。

```typescript
// Insert in the same constraints block(near chk_tasks_close_result):
check('chk_tasks_executor_type', sql`
  executor_type IS NULL OR executor_type = ANY(ARRAY['human', 'ai_agent', 'system'])
`),
```

- [ ] **Step 9: Run test to verify it passes**

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test tests/schema/tasks-phase1.test.ts
```

Expected: PASS — 4 tests pass

- [ ] **Step 10: Run full test suite to verify no regression**

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test
```

Expected: All existing tests + 2 new test files pass. If existing tests fail because they assumed 2-value `TASK_STATUS`, update those tests to accept 3-value transitional enum.

- [ ] **Step 11: Commit**

```bash
cd /Users/maxwsy/workspace/callytics-common
git add src/db/schema/tasks.ts tests/schema/tasks-phase1.test.ts
git commit -m "feat(schema): tasks Phase 1 transitional — 3-value status + new columns + create_closed dedup

Phase 1 Plan 01 Task 2.

- TASK_STATUS = ['pending','open','closed'] (3-value transitional;
  Plan 07 cutover shrinks to ['open','closed'])
- TASK_CLOSE_TYPE += 'create_closed'
- TASK_CLOSE_RESULT += 'unable_to_reach' (19-value transitional;
  Plan 07 cutover removes 4 progress values → 15-value final)
- New columns: executor_type, attempt_count (NOT NULL DEFAULT 0), source_call_id
- chk_tasks_closed_integrity 接受 status IN ('pending','open')
- chk_tasks_close_result 加 unable_to_reach
- uq_tasks_pending_contact_category WHERE 改 status IN ('pending','open')
- NEW: uq_tasks_create_closed_evidence partial unique
  防同一通电话(source_call_id)重复 create_closed task

Spec: docs/product-design/v2/unified-pipeline/implementation-plan/normative-spec.md §2.2, §6.2
"
```

***

## Task 3: Update `task-ui.ts` — STATUS\_OPTIONS / CLOSE\_RESULT\_OPTIONS / new PROGRESS\_OPTIONS

**Files:**

- Modify: `src/db/schema/task-ui.ts`

- Test: `tests/schema/task-ui-phase1.test.ts`

- [ ] **Step 1: Write the failing test**

Create `tests/schema/task-ui-phase1.test.ts`:

```typescript
import { describe, expect, test } from 'bun:test';
import {
  STATUS_OPTIONS,
  CLOSE_RESULT_OPTIONS,
  PROGRESS_OPTIONS,
} from '../../src/db/schema/task-ui';

describe('task-ui phase 1', () => {
  test('STATUS_OPTIONS includes open with label Open', () => {
    const openOpt = STATUS_OPTIONS.find((o) => o.value === 'open');
    expect(openOpt).toBeDefined();
    expect(openOpt?.label).toBe('Open');
  });

  test('CLOSE_RESULT_OPTIONS includes unable_to_reach', () => {
    const opt = CLOSE_RESULT_OPTIONS.find((o) => o.value === 'unable_to_reach');
    expect(opt).toBeDefined();
    expect(opt?.label).toBe('Unable to Reach');
  });

  test('PROGRESS_OPTIONS lists 6 values with UI labels', () => {
    expect(PROGRESS_OPTIONS.length).toBe(6);
    const values = PROGRESS_OPTIONS.map((o) => o.value);
    expect(values).toEqual([
      'no_answer',
      'left_voicemail',
      'text_sent',
      'callback_requested',
      'follow_up_scheduled',
      'customer_considering',
    ]);
    const noAnswer = PROGRESS_OPTIONS.find((o) => o.value === 'no_answer');
    expect(noAnswer?.label).toBe('No Answer');
  });
});
```

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

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test tests/schema/task-ui-phase1.test.ts
```

Expected: FAIL — `STATUS_OPTIONS` has no 'open' entry;`CLOSE_RESULT_OPTIONS` has no 'unable\_to\_reach';`PROGRESS_OPTIONS` undefined export

- [ ] **Step 3: Update `STATUS_OPTIONS`**

Read current `STATUS_OPTIONS` (around line 59 of `task-ui.ts`) and add the `'open'` entry. Keep `'pending'` during transitional window:

```typescript
// BEFORE (assuming current shape):
export const STATUS_OPTIONS: readonly StatusOption[] = [
  { value: 'pending', label: 'Open' },
  { value: 'closed', label: 'Closed' },
] as const;

// AFTER (transitional — both 'pending' and 'open' map to UI label "Open"):
export const STATUS_OPTIONS: readonly StatusOption[] = [
  // Legacy value during Phase 1 transitional window; final cutover (Plan 07)
  // removes 'pending' entry. UI label stays "Open" for both during transition.
  { value: 'pending', label: 'Open' },
  { value: 'open', label: 'Open' },
  { value: 'closed', label: 'Closed' },
] as const;
```

- [ ] **Step 4: Add `unable_to_reach` to `CLOSE_RESULT_OPTIONS`**

Append to the existing `CLOSE_RESULT_OPTIONS` array:

```typescript
// Add this entry to the array (preserve all existing entries):
{ value: 'unable_to_reach', label: 'Unable to Reach', category: 'negative', description: 'Max attempts reached, contact could not be reached' },
```

- [ ] **Step 5: Add new `PROGRESS_OPTIONS` export**

After `CLOSE_RESULT_OPTIONS`, add:

```typescript
/**
 * Progress 类型 UI label 列表。
 * 对应 task_progress_events.progress_type enum(TASK_PROGRESS_TYPE in
 * task-progress-events.ts)。staff 用 progress endpoint 时前端 dropdown 用这个。
 *
 * 注意:no_answer / left_voicemail 现在是 progress(不再是 closeResult)。
 * 旧 UI 把这两个 + callback_later 放在 close dropdown 里,Phase 1 后必须移到 progress dropdown。
 */
export interface ProgressOption {
  value: TaskProgressType;
  label: string;
  description: string;
}

import type { TaskProgressType } from './task-progress-events';

export const PROGRESS_OPTIONS: readonly ProgressOption[] = [
  { value: 'no_answer', label: 'No Answer', description: 'Called but no one picked up' },
  { value: 'left_voicemail', label: 'Left Voicemail', description: 'Left a voicemail message' },
  { value: 'text_sent', label: 'Text Sent', description: 'Sent SMS, waiting for reply' },
  { value: 'callback_requested', label: 'Callback Requested', description: 'Customer asked to be called back' },
  { value: 'follow_up_scheduled', label: 'Follow-up Scheduled', description: 'Staff scheduled next contact attempt' },
  { value: 'customer_considering', label: 'Customer Considering', description: 'Customer needs time to think' },
] as const;
```

- [ ] **Step 6: Run test to verify it passes**

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test tests/schema/task-ui-phase1.test.ts
```

Expected: PASS — 3 tests pass

- [ ] **Step 7: Commit**

```bash
cd /Users/maxwsy/workspace/callytics-common
git add src/db/schema/task-ui.ts tests/schema/task-ui-phase1.test.ts
git commit -m "feat(schema): task-ui Phase 1 — STATUS_OPTIONS adds 'open' + PROGRESS_OPTIONS + unable_to_reach

Phase 1 Plan 01 Task 3.

- STATUS_OPTIONS: 'pending' 和 'open' 双值并存(transitional);UI label 同为 'Open'
- CLOSE_RESULT_OPTIONS += unable_to_reach
- NEW: PROGRESS_OPTIONS 6 values for staff progress endpoint dropdown

Spec: normative-spec.md §2.2
"
```

***

## Task 3.5: Add `task.progress_recorded` to `TIMELINE_EVENT_TYPES`

**Files:**

- Modify: `src/db/schema/contact-timeline.ts`(加 1 个 enum value)
- Modify: `tests/schema/contact-timeline.test.ts`(若有,同步)

**Why**: Plan 06(studio-api)record\_progress endpoint emit `eventType='task.progress_recorded'` timeline event。但 live `contact-timeline.ts:21-49` 的 `TIMELINE_EVENT_TYPES` 不含此值,DB CHECK constraint 会 reject INSERT。Codex audit 抓到的 missing。

- [ ] **Step 1: Locate TIMELINE\_EVENT\_TYPES const**

Read `src/db/schema/contact-timeline.ts` lines 21-49 — find the const array.

- [ ] **Step 2: Add `task.progress_recorded` 到 enum array**

```typescript
// BEFORE: 15 values(verbatim list per current schema)
export const TIMELINE_EVENT_TYPES = [
  'call.completed',
  'message.received',
  // ... 13 more
] as const;

// AFTER: 16 values(加 'task.progress_recorded')
export const TIMELINE_EVENT_TYPES = [
  'call.completed',
  'message.received',
  // ... 13 more
  'task.progress_recorded',  // NEW(Phase 1 Plan 06 record_progress endpoint)
] as const;
```

- [ ] **Step 3: Update DB CHECK constraint(if separate from enum)**

If `contact-timeline.ts` has a `check('chk_timeline_event_type', ...)` constraint, **必须同步加** `'task.progress_recorded'`。否则 DB 仍 reject。

- [ ] **Step 4: Migration SQL**

Add to `drizzle/0010_phase1_transitional.sql`:

```sql
-- Phase 1: add task.progress_recorded to timeline event types
ALTER TABLE contact_timeline DROP CONSTRAINT IF EXISTS chk_timeline_event_type;
ALTER TABLE contact_timeline ADD CONSTRAINT chk_timeline_event_type CHECK (
  event_type = ANY(ARRAY[
    -- ... existing 15 values verbatim ...
    'task.progress_recorded'  -- NEW
  ])
);
```

- [ ] **Step 5: Run schema test + integration**

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test contact-timeline
```

Expected: PASS — TIMELINE\_EVENT\_TYPES includes new value, INSERT with `event_type='task.progress_recorded'` succeeds.

- [ ] **Step 6: Commit**

```bash
cd /Users/maxwsy/workspace/callytics-common
git add src/db/schema/contact-timeline.ts drizzle/0010_phase1_transitional.sql
git commit -m "feat(schema): contact_timeline + 'task.progress_recorded' event type

Phase 1 Plan 01 Task 3.5(Codex audit 2026-06-02 抓的 missing).

Plan 06 studio-api record_progress endpoint emit
eventType='task.progress_recorded' timeline event(see normative-spec.md §3.5 line 417-420)。
live TIMELINE_EVENT_TYPES + DB CHECK 没此值 → INSERT 被 reject。

加 enum + 同步 DB CHECK 解 missing。
"
```

***

## Task 4: Export `task-progress-events` from barrel `index.ts`

**Files:**

- Modify: `src/db/schema/index.ts`

- [ ] **Step 1: Read current barrel file**

Read `src/db/schema/index.ts` to see current export pattern.

- [ ] **Step 2: Add export for task-progress-events**

Add this line in `src/db/schema/index.ts`, following the same pattern as other schema exports (alphabetical placement):

```typescript
export * from './task-progress-events';
```

- [ ] **Step 3: Verify package builds**

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && bun run build
```

Expected: TypeScript compiles without errors. `dist/db/schema/task-progress-events.js` exists.

- [ ] **Step 4: Verify external consumer can import**

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common && cat << 'EOF' | bun run --bun -
import { taskProgressEvents, TASK_PROGRESS_TYPE } from './src/db/schema';
console.log('taskProgressEvents keys:', Object.keys(taskProgressEvents).slice(0, 5));
console.log('TASK_PROGRESS_TYPE:', TASK_PROGRESS_TYPE);
EOF
```

Expected: prints column names + 6 enum values without errors.

- [ ] **Step 5: Commit**

```bash
cd /Users/maxwsy/workspace/callytics-common
git add src/db/schema/index.ts
git commit -m "feat(schema): export task-progress-events from barrel

Phase 1 Plan 01 Task 4.

让 caller(callytics-infrastructure / studio-api / lead-tracking)能
import { taskProgressEvents, TASK_PROGRESS_TYPE, ... } from '@retaintive/common/db'。
"
```

***

## Task 5: Generate Drizzle migration SQL `drizzle/0010_phase1_transitional.sql`

**Files:**

- Create: `drizzle/0010_phase1_transitional.sql`

- (Auto-generated) `drizzle/meta/_journal.json` will be updated by drizzle-kit

- [ ] **Step 1: Set DATABASE\_URL to test Neon**

Confirm `DATABASE_URL` env var points to test Neon:

```bash
cd /Users/maxwsy/workspace/callytics-common
echo $DATABASE_URL | grep -q neon && echo "OK: Neon URL set" || echo "MISSING: set DATABASE_URL to test Neon connection string"
```

Expected: prints `OK: Neon URL set`. If missing, source `.env` or export the URL before continuing.

- [ ] **Step 2: Generate migration**

Run drizzle-kit to generate migration SQL from the updated schema:

```bash
cd /Users/maxwsy/workspace/callytics-common
bunx drizzle-kit generate --name=phase1_transitional
```

Expected: a new file `drizzle/0010_phase1_transitional.sql` is created. drizzle-kit prints `Saved 1 migration`. `_journal.json` updated.

- [ ] **Step 3: Inspect generated SQL**

```bash
cd /Users/maxwsy/workspace/callytics-common && cat drizzle/0010_phase1_transitional.sql
```

Verify the SQL contains:

1. `CREATE TABLE "task_progress_events"` with all 15 columns
2. `CREATE UNIQUE INDEX ... idempotency_key`
3. `CREATE INDEX "idx_tpe_task_occurred"` (and 3 more)
4. `ALTER TABLE "tasks" ADD COLUMN "executor_type"` etc.
5. `ALTER TABLE "tasks" DROP CONSTRAINT "chk_tasks_closed_integrity"` (then re-create with transitional CHECK)
6. `DROP INDEX "uq_tasks_pending_contact_category"` (then re-create with transitional WHERE)
7. `CREATE UNIQUE INDEX "uq_tasks_create_closed_evidence"`

If any of these are missing, the schema changes are incomplete. Stop and fix the schema file before continuing.

- [ ] **Step 4: Hand-review the generated SQL for safety**

Specifically verify:

- No `DROP COLUMN` statements (this migration is **additive only** — destructive DROP belongs to Plan 07 cutover)
- No `UPDATE` data statements (data migration also belongs to Plan 07)
- No `ALTER COLUMN ... SET NOT NULL` on `tasks.store_id` (still nullable during transitional)

If drizzle-kit generated any of these, **manually edit the SQL file** to comment out / remove. Add a header comment at the top:

```sql
-- Phase 1 Plan 01 — ADDITIVE-ONLY migration
-- 不包含任何 DROP COLUMN / UPDATE data / store_id SET NOT NULL。
-- 那些 destructive 操作由 Plan 07 cutover migration 单独执行。
-- Spec: docs/product-design/v2/unified-pipeline/implementation-plan/normative-spec.md §6.2
```

- [ ] **Step 5: Apply migration to test Neon**

```bash
cd /Users/maxwsy/workspace/callytics-common
bunx drizzle-kit push
```

Expected: drizzle-kit prints `Changes applied`. No errors.

- [ ] **Step 6: Verify schema in Neon**

```bash
cd /Users/maxwsy/workspace/callytics-common
psql $DATABASE_URL -c "\d task_progress_events"
psql $DATABASE_URL -c "\d tasks" | grep -E "executor_type|attempt_count|source_call_id"
psql $DATABASE_URL -c "SELECT conname FROM pg_constraint WHERE conrelid = 'tasks'::regclass AND conname LIKE 'chk_%';"
psql $DATABASE_URL -c "SELECT indexname FROM pg_indexes WHERE tablename = 'tasks' AND indexname LIKE 'uq_%';"
```

Expected output:

- `task_progress_events` exists with 15 columns + 1 unique constraint(idempotency\_key)+ 4 indexes
- `tasks` has new columns `executor_type / attempt_count / source_call_id`
- CHECK constraints `chk_tasks_closed_integrity` exists with `status IN ('pending','open')`
- partial unique `uq_tasks_pending_contact_category` exists with updated WHERE clause
- new partial unique `uq_tasks_create_closed_evidence` exists

If any verification fails, rollback by checking out the migration file before push, then re-generate.

- [ ] **Step 6.b: Add DB invariant tests for transitional constraints**

Create / update `tests/integration/tasks-transitional-schema.test.ts`.

These tests verify application-level schema invariants that are easy to regress when the generated SQL is hand-edited:

| # | Invariant                                                            | Test setup                                                                                                  | Expected         |
| - | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------- |
| 1 | `chk_tasks_closed_integrity` accepts transitional open states        | INSERT `status='pending'` with all close fields null; INSERT `status='open'` with all close fields null     | both pass        |
| 2 | `chk_tasks_closed_integrity` rejects invalid closed rows             | INSERT `status='closed'` without `closed_at` / `close_type` / `close_result`                                | CHECK violation  |
| 3 | `uq_tasks_pending_contact_category` covers both `pending` and `open` | INSERT one `pending` task then one `open` task for same `(contact_phone, store_id, type_category)`          | UNIQUE violation |
| 4 | `uq_tasks_create_closed_evidence` enforces closed evidence dedupe    | INSERT two closed `create_closed` rows with same `(contact_phone, store_id, type_category, source_call_id)` | UNIQUE violation |

Run:

```bash
cd /Users/maxwsy/workspace/callytics-common
bun test tests/integration/tasks-transitional-schema.test.ts
```

Expected: all PASS. Do not replace these with `psql \d` checks; `\d` verifies shape, not behavior.

- [ ] **Step 7: Commit migration file**

```bash
cd /Users/maxwsy/workspace/callytics-common
git add drizzle/0010_phase1_transitional.sql drizzle/meta/_journal.json
git commit -m "feat(migration): 0010 Phase 1 transitional — additive only

Phase 1 Plan 01 Task 5.

ADDITIVE only:
- CREATE TABLE task_progress_events (15 columns + 4 indexes + idempotency_key UNIQUE)
- ALTER TABLE tasks ADD COLUMN executor_type / attempt_count / source_call_id
- Recreate chk_tasks_closed_integrity accepting status IN ('pending','open')
- Recreate chk_tasks_close_result with unable_to_reach
- Recreate uq_tasks_pending_contact_category WHERE status IN ('pending','open')
- CREATE UNIQUE INDEX uq_tasks_create_closed_evidence

不包含 DROP COLUMN / UPDATE data / store_id SET NOT NULL。
那些 destructive 操作由 Plan 07 cutover 执行。

Applied to test Neon manually via drizzle-kit push.
Spec: normative-spec.md §6.2
"
```

***

## Task 6: Bump `@retaintive/common` version + publish to GitHub Packages

**Files:**

- Modify: `package.json:5` (version field)

- [ ] **Step 1: Decide version bump**

Current version: read `package.json`:

```bash
cd /Users/maxwsy/workspace/callytics-common && jq -r .version package.json
```

This Phase 1 change adds new exports and new columns to existing table — semver **minor** bump. If current is `1.0.0`, bump to `1.1.0`.

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

Edit `package.json`. Replace `"version": "1.0.0"` (or whatever the current value is) with `"version": "1.1.0"`.

- [ ] **Step 3: Verify build artifact has new exports**

```bash
cd /Users/maxwsy/workspace/callytics-common
bun run build
grep -r "task-progress-events" dist/ | head -5
```

Expected: at least one match in `dist/db/schema/index.d.ts` or similar.

- [ ] **Step 4: Run full test suite one more time**

```bash
cd /Users/maxwsy/workspace/callytics-common && bun test
```

Expected: all tests pass, including the 3 new schema test files.

- [ ] **Step 5: Commit version bump**

```bash
cd /Users/maxwsy/workspace/callytics-common
git add package.json
git commit -m "chore: bump @retaintive/common to 1.1.0

Phase 1 Plan 01 Task 6.

Minor bump for additive Phase 1 schema:
- new exports: taskProgressEvents, TASK_PROGRESS_TYPE,
  TASK_CHANNEL, TASK_ACTOR_TYPE, PROGRESS_OPTIONS
- new columns: tasks.executor_type, tasks.attempt_count, tasks.source_call_id
- enum widening: TASK_STATUS, TASK_CLOSE_TYPE, TASK_CLOSE_RESULT

All Phase 1 caller PRs (Plans 02-06) pin this version.
"
```

- [ ] **Step 6: Push branch + create PR**

```bash
cd /Users/maxwsy/workspace/callytics-common
git push -u origin <feature-branch-name>
gh pr create --title "feat(schema): Phase 1 — task_progress_events + tasks transitional schema" \
  --body-file <(cat << 'EOF'
## Phase 1 Plan 01

Implements the additive schema changes for unified-pipeline Phase 1.

### What's added

- New table `task_progress_events` (15 columns, idempotency_key UNIQUE, 4 indexes)
- New `tasks` columns: `executor_type`, `attempt_count`, `source_call_id`
- New enums: `TASK_PROGRESS_TYPE`, `TASK_CHANNEL`, `TASK_ACTOR_TYPE`
- Enum widening: `TASK_STATUS` += `'open'`, `TASK_CLOSE_TYPE` += `'create_closed'`, `TASK_CLOSE_RESULT` += `'unable_to_reach'`
- New partial unique: `uq_tasks_create_closed_evidence`
- Transitional CHECK / partial unique WHERE clauses accept both `'pending'` and `'open'`

### What's NOT here

- No DROP COLUMN
- No `UPDATE pending → open`
- No `tasks.store_id SET NOT NULL`

Those destructive operations are Plan 07 cutover, only after Plans 02-06 caller migrations all ship.

### Test

- 3 new schema unit test files, all pass
- Migration applied to test Neon, verified via `psql \d` + index inspection
- Full test suite green

### Spec

See `docs/product-design/v2/unified-pipeline/implementation-plan/normative-spec.md` §2 + §6.2 in the docs repo.
EOF
)
```

After PR merges, downstream caller plans (02-06) bump `@retaintive/common` dependency to `1.1.0`.

***

## Self-Review Checklist (skill requires)

Run through this once Plan 01 is fully written, before declaring done:

### Spec coverage

Skim `normative-spec.md` §2 + §6.2:

- ✅ §2.1 `task_progress_events` CREATE TABLE → Task 1 + Task 5
- ✅ §2.2 `tasks.status` enum widen → Task 2 Step 3
- ✅ §2.2 new `executor_type / attempt_count / source_call_id` columns → Task 2 Step 6
- ✅ §2.2 `TASK_CLOSE_TYPE` += `create_closed` → Task 2 Step 4
- ✅ §2.2 `TASK_CLOSE_RESULT` += `unable_to_reach` → Task 2 Step 5
- ✅ §2.2 create\_closed partial unique → Task 2 Step 8
- ✅ §2.2 chk\_tasks\_closed\_integrity transitional CHECK → Task 2 Step 7
- ✅ §2.4 `task-ui.ts` STATUS\_OPTIONS / PROGRESS\_OPTIONS → Task 3
- ✅ §2.4 barrel export → Task 4
- ✅ §6.2 transitional (no destructive in this plan) → All tasks emphasize "additive only"
- ⚠️ §2.2 `tasks.store_id SET NOT NULL` → **Not in this plan** (correct — belongs to Plan 07 cutover)
- ⚠️ §2.2 DROP COLUMN action\_needed / task\_type → **Not in this plan** (correct — Plan 07)

### Placeholder scan

- No `TBD` / `TODO` / `implement later` strings
- Every code block contains the actual code (no `...` truncations except clearly-marked "existing comment" blocks where reader is told to keep current content)
- Every test has actual `expect(...)` assertions, not "write tests for the above"

### Type consistency

- `TaskProgressType` used in Task 3 matches the type defined in Task 1
- `taskProgressEvents` table name used in Task 4 matches the export from Task 1
- `executor_type` / `attempt_count` / `source_call_id` snake\_case in SQL / camelCase in TS — consistent across Task 2, 5, and Self-Review

### Outstanding gaps

None identified. Plan 01 is self-contained and produces shippable software:

- After all 6 tasks: `@retaintive/common` 1.1.0 is published with additive schema
- callytics-infrastructure / studio-api / lead-tracking can update their dependency and start using the new exports in Plans 02-06
- test Neon has new table + new columns + transitional CHECKs, ready for Plan 02 module implementation against it

***

## Execution Handoff

After saving the plan, the user picks an execution mode:

**Option 1: Subagent-Driven (recommended)** — dispatch a fresh subagent per task, review between tasks, fast iteration. Sub-skill: `superpowers:subagent-driven-development`.

**Option 2: Inline Execution** — execute tasks in the current session using `superpowers:executing-plans`, batch execution with checkpoints.

When ready to execute, tell the agent which mode to use.
