Tasks: 17 — Recruiter / LinkedIn Outreach (draft-only)
Status legend:
[ ]pending •[~]in-progress •[x]done •[-]dropped
Phase 1 — Structured contract + persistence
-
T01 — Define the
outreachstructured artifact (Zod machine summary)- Files:
packages/ai/src/structured/schemas/outreach.ts(new); export frompackages/ai/src/structured/index.ts - Acceptance:
outreachArtifactdefined viadefineArtifactfrompackages/ai/src/structured/contract.ts(same pattern asschemas/evaluation.ts).- Summary captures:
contactTypeenum (connection_note | follow_up | referral_ask),targetRole(.max()string),targetCompany(.max()string), framework object{ hook, credibility, ask }each a bounded string,groundedSourcesandusedFactsarrays (.max()-bounded), and anOUTREACH_SCHEMA_VERSIONconstant. - All strings/arrays carry
.max()bounds (Article 8.3); types exported fromindex.ts.
- Estimate: 0.5 day
- Files:
-
T02 — Unit test the
outreachartifact schema- Files:
packages/ai/src/structured/schemas/outreach.test.ts(new) - Acceptance:
- Valid artifact parses; missing/oversized framework lines reject.
- Invalid
contactTyperejects;usedFacts/groundedSourcesover.max()reject. assertArtifact(outreachArtifact, ...)round-trips a valid object.pnpm test -- --selectProjects aigreen for this file.
- Estimate: 0.5 day
- Files:
-
T03 — Add
outreachDraftstable + export- Files:
packages/db/src/schema/outreach.ts(new); export frompackages/db/src/schema/index.ts - Acceptance:
outreachDraftsfollows house style:integer("id").primaryKey().generatedAlwaysAsIdentity();userIdtext FK →users.idonDelete: "cascade"; optionaljobIdinteger FK →jobs.id;contactTypetext enum.notNull(); jsonbframework/summary$type<>()mirroring the T01 artifact;created_at/updated_atdefaultNow().- Indexes
outreach_drafts_user_id_idxandoutreach_drafts_user_job_idxvia(table) => [...]. - Exported from
schema/index.ts;packages/dbtype-checks.
- Estimate: 0.5 day
- Files:
-
T04 — Push schema + unit-test table types
- Files:
packages/db/src/schema/outreach.test.ts(new); runpnpm db:push - Acceptance:
pnpm db:pushapplies theoutreach_draftstable with no diff/error.- Test asserts column set, enum values, FK/cascade config, and index names.
pnpm test -- --selectProjects dbgreen.
- Estimate: 0.5 day
- Files:
Phase 2 — outreachDraft tool + orchestrator wiring
-
T05 — Implement the
outreachDrafttool (grounded, draft-only)- Files:
packages/ai/src/tools/outreach-draft.ts(new) - Acceptance:
tool({ description, inputSchema, execute })with inputsjobId?: number,companyName?: string(.max(200)),contactTypeenum, optionaltone/focusAreas(.max()-bounded);userIdis optional/server-injected, never LLM-driven (mirrorsgenerate-cover-letter.ts).executereads the user profile + thejobsrow viaimport { db } from "@ever-hust/db", computes matching skills, and returns a grounded context object + aninstructionthat enforces the hook→credibility→ask framework and forbids invented facts/placeholders (Article 7).- Returns a Zod-validated
outreachartifact (viarunValidatedGeneration/assertArtifact) alongside prose context (Article 5). - Output is draft-only: no send/connect/submit code path; on missing user/job returns a safe
{ error }object like the sibling tools.
- Estimate: 1 day
- Files:
-
T06 — Persist generated drafts to
outreachDrafts- Files:
packages/ai/src/tools/outreach-draft.ts(extend T05) - Acceptance:
- On a successful draft, inserts one row into
outreachDraftswithuserId, optionaljobId,contactType,framework, and the validatedsummary. - Insert failure is caught and does not break the chat turn (logs + returns the draft anyway).
- Covered by a unit test (see T08).
- On a successful draft, inserts one row into
- Estimate: 0.5 day
- Files:
-
T07 — Export + register the tool in the orchestrator (server-side
userId)- Files:
packages/ai/src/tools/index.ts;packages/ai/src/agents/orchestrator.ts - Acceptance:
outreachDraftToolexported fromtools/index.ts.- Added to the
tools: { ... }object increateOrchestratorStreamwith the{ ...params, userId }server-injection wrapper (same asgenerateCoverLetter/applyJob). userIdnever appears as an LLM-supplied param;stopWhen stepCountIs(5)unchanged.pnpm check-typesgreen; orchestrator test (if present) lists the new tool.
- Estimate: 0.5 day
- Files:
-
T08 — Unit test the
outreachDrafttool- Files:
packages/ai/src/tools/outreach-draft.test.ts(new) - Acceptance:
- Asserts the returned context contains only grounded fields (no fabricated name/connection),
that
instructionrequests the 3-line framework and forbids placeholders. - Asserts the emitted artifact validates against the T01 schema and
contactTypeis echoed. - Asserts a
outreachDraftsinsert is attempted (db mocked) and a draft is still returned if the insert throws. pnpm test -- --selectProjects aigreen.
- Asserts the returned context contains only grounded fields (no fabricated name/connection),
that
- Estimate: 0.5 day
- Files:
-
T09 — Document the tool in the system prompt
- Files:
packages/ai/src/prompts.ts(DEFAULT_ORCHESTRATOR_PROMPT); note the same edit for the Langfuseorchestrator-systemproduction prompt - Acceptance:
- New
## Outreach Draftssection: listsoutreachDraftunder capabilities, states the hook→credibility→ask framework, the draft-only / HITL rule (Hust sends nothing; user copies), and instructs the agent to confirm role/company/contact-type before drafting. packages/ai/src/prompts.test.tsupdated/green asserting the section/tool name is present.
- New
- Estimate: 0.5 day
- Files:
-
T10 — (Conditional) Add
checkOutreachLimitgate- Files:
packages/ai/src/rate-limit.ts; wrapper inpackages/ai/src/agents/orchestrator.ts - Acceptance:
- If gated (per plan Open Question 1), add
checkOutreachLimit(userId)mirroringcheckCoverLetterLimit; orchestrator wrapper returns an upgrade-style{ error, requiresUpgrade }when over-limit for non-subscribed users. - Unit test in
packages/ai/src/rate-limit.test.tscovers allow/deny. - Drop this task (
[-]) if the decision is ungated free-tier.
- If gated (per plan Open Question 1), add
- Estimate: 0.5 day
- Files:
Phase 3 — Canvas surface, read API, copy-to-send, E2E
-
T11 — Add canvas-sync case for the outreach draft
- Files:
apps/web/hooks/use-canvas-sync.ts - Acceptance:
CanvasStategainsoutreachDraft: OutreachDraftData | null.handleToolResultgetscase "outreachDraft"that sets the draft into state (guarding on a valid framework, like thesalaryInsightscase).- A
clearOutreachDraftcallback is exported alongsideclearSalaryInsights.
- Estimate: 0.5 day
- Files:
-
T12 — Build the outreach draft canvas card (copy-to-send, no Send)
- Files:
apps/web/components/canvas/outreach-draft-card.tsx(new); wire intoapps/web/app/(dashboard)/chat/page.tsx - Acceptance:
- Modelled on
salary-insights-card.tsx:@ever-hust/ui/card+@ever-hust/ui/button+cn(); renders the three framework lines (hook/credibility/ask) and target role/company. - A Copy to clipboard button copies the assembled message; shows copied confirmation.
- No Send/Connect control exists (Article 4); card has a close/clear affordance.
- Rendered on the canvas when
outreachDraftis set.
- Modelled on
- Estimate: 1 day
- Files:
-
T13 — Read API route for saved drafts
- Files:
apps/web/app/api/outreach/route.ts(new); Zod inapps/web/lib/api-schemas.ts; usesapps/web/lib/api-response.ts - Acceptance:
- GET-only:
requireSessionUser(),applyRateLimit(userId, "authenticated"), returns the caller'soutreachDraftsrows (scoped byuserId); invalid query →apiBadRequest(). - No POST/PUT send or submit handler anywhere in the route.
- Default cache-control headers applied per the route pattern.
- GET-only:
- Estimate: 0.5 day
- Files:
-
T14 — Unit test route + schema
- Files:
apps/web/lib/api-schemas.test.ts(extend);apps/web/app/api/outreach/route.test.ts(new,web-libproject) - Acceptance:
- Zod query schema accepts valid params, rejects malformed ones.
- Route returns 401 unauthenticated, scopes results to the session user, and exposes no send/submit verb.
pnpm test -- --selectProjects web-libgreen.
- Estimate: 0.5 day
- Files:
-
T15 — Playwright E2E: draft → card → copy, HITL assertion
- Files:
tests/e2e/outreach.spec.ts(new) - Acceptance:
- Unauthenticated
/chataccess redirects to login (route protection, likechat.spec.ts). - Authenticated flow: asking for an outreach draft renders the outreach card with the three framework lines and a working Copy affordance.
- Asserts no auto-send/connect control is present on the card (the structural HITL gate).
pnpm test:e2egreen.
- Unauthenticated
- Estimate: 1 day
- Files:
-
T16 — Full CI green + roadmap update
- Files:
docs/specs/ROADMAP.md(progress); runpnpm lint && pnpm check-types && pnpm test && pnpm test:e2e - Acceptance:
pnpm lint(eslint--max-warnings 0),pnpm check-types,pnpm test,pnpm test:e2eall green.- Competitor-name grep over the diff is empty (Article 11).
docs/specs/ROADMAP.mdmarks epic 17 progress.
- Estimate: 0.5 day
- Files:
Notes
- Write tests alongside each implementation task; do not batch testing into a final task.
- Verify zero competitor references before every commit (see constitution Article 11).
- Draft-only / HITL is structural: there is no send/connect/submit code path in any task here.
userIdis always server-injected by the orchestrator — never an LLM-supplied tool param.- Update
docs/specs/ROADMAP.mdprogress when an epic's tasks complete.