Are you an LLM? View /llms.txt for optimized Markdown documentation, or /llms-full.txt for full documentation bundle. This page is also available as Markdown at /product-design/v2/unified-pipeline/implementation-plan/plans/2026-06-02-01-callytics-common-schema.md
#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
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:
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:
// 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;
Edit src/db/schema/tasks.ts lines 38-54. Add 'unable_to_reach' to the array:
// BEFORE: 18 valuesexport 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):
Edit src/db/schema/tasks.ts line 262 — replace chk_tasks_closed_integrity:
// 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':
Edit src/db/schema/tasks.ts line 254 — uq_tasks_pending_contact_category WHERE clause:
// 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:
// #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 都绕过)。
// 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:
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:
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
cd /Users/maxwsy/workspace/callytics-commongit add src/db/schema/tasks.ts tests/schema/tasks-phase1.test.tsgit commit -m "feat(schema): tasks Phase 1 transitional — 3-value status + new columns + create_closed dedupPhase 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 taskSpec: docs/product-design/v2/unified-pipeline/implementation-plan/normative-spec.md §2.2, §6.2"
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:
// 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:
// 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:
/** * 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:
cd /Users/maxwsy/workspace/callytics-common && bun test tests/schema/task-ui-phase1.test.ts
(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:
cd /Users/maxwsy/workspace/callytics-commonecho $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:
cd /Users/maxwsy/workspace/callytics-commonbunx 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
cd /Users/maxwsy/workspace/callytics-common && cat drizzle/0010_phase1_transitional.sql
Verify the SQL contains:
CREATE TABLE "task_progress_events" with all 15 columns
CREATE UNIQUE INDEX ... idempotency_key
CREATE INDEX "idx_tpe_task_occurred" (and 3 more)
ALTER TABLE "tasks" ADD COLUMN "executor_type" etc.
ALTER TABLE "tasks" DROP CONSTRAINT "chk_tasks_closed_integrity" (then re-create with transitional CHECK)
DROP INDEX "uq_tasks_pending_contact_category" (then re-create with transitional WHERE)
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:
-- 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
cd /Users/maxwsy/workspace/callytics-commonbunx drizzle-kit push
Expected: drizzle-kit prints Changes applied. No errors.
Step 6: Verify schema in Neon
cd /Users/maxwsy/workspace/callytics-commonpsql $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_%';"
INSERT two closed create_closed rows with same (contact_phone, store_id, type_category, source_call_id)
UNIQUE violation
Run:
cd /Users/maxwsy/workspace/callytics-commonbun 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
cd /Users/maxwsy/workspace/callytics-commongit add drizzle/0010_phase1_transitional.sql drizzle/meta/_journal.jsongit commit -m "feat(migration): 0010 Phase 1 transitional — additive onlyPhase 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:
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
cd /Users/maxwsy/workspace/callytics-commonbun run buildgrep -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
cd /Users/maxwsy/workspace/callytics-common && bun test
Expected: all tests pass, including the 3 new schema test files.
Step 5: Commit version bump
cd /Users/maxwsy/workspace/callytics-commongit add package.jsongit commit -m "chore: bump @retaintive/common to 1.1.0Phase 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_RESULTAll Phase 1 caller PRs (Plans 02-06) pin this version."
Step 6: Push branch + create PR
cd /Users/maxwsy/workspace/callytics-commongit 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 01Implements 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### SpecSee `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.
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"
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.