R&D AI Helper — Facet Parity Spec

Build-ready spec for Codex · Linear → Workforce mapping · v0.4 · 2026-09-18
30 Linear facets · 22 built · 8 dropped · 6 Workforce-only Phase P0 · shippable in 4 weeks
§1 · Scope + non-goals §2 · Data model summary §3 · Facet parity table (all 30 + 6) §4 · Workforce-only extras — deep spec §5 · API surface (v1) §6 · Phasing — what Codex ships in what order §7 · Test contracts + acceptance §8 · Explicitly out of scope

§1 · Scope + non-goals

In scope. Ship a Linear-shape scheduling module inside Workforce, sufficient to replace Linear Standard for an R&D team of 5–50 engineers. Ships as a first-class Workforce module — no separate app, no separate signup, no separate billing.

Non-goals. Not building: Insights custom-analytics builder, customer changelog, Docs (Notion-like pages), Initiatives (higher-order project grouping), SLA rules, more than one level of subtask nesting, customer requests, marketplace / third-party integration hub. Rationale per facet in §3 and §8.

Two-way Linear sync. One-shot import + 14-day read-only safety net only. Not permanent coexistence (defeats the "cancel Linear" outcome).

§2 · Data model summary

Postgres, new schema wf_scheduling. All tables carry subscriber_id uuid NOT NULL for tenant isolation.

-- Core tables (P0)
wf_scheduling.teams            (id, subscriber_id, key, name, cycle_length_days, created_at)
wf_scheduling.cycles           (id, team_id, number, starts_at, ends_at, state)
wf_scheduling.projects         (id, subscriber_id, name, description, team_id NULL, target_date, state)
wf_scheduling.milestones       (id, project_id, name, target_date, order_idx, state)
wf_scheduling.tasks            (id, subscriber_id, team_id, cycle_id NULL, project_id NULL,
                                identifier text,          -- 'KL-152'
                                title text, description text,
                                state text,               -- backlog|todo|in_progress|in_review|done|canceled
                                priority smallint,        -- 0..4  (no|urgent|high|med|low)
                                assignee_user_id uuid,
                                estimate_hours numeric,   -- signed required (see wf-only extras)
                                parent_task_id uuid,      -- exactly 1 level of nesting
                                created_at, updated_at, completed_at)
wf_scheduling.labels           (id, team_id, name, color)
wf_scheduling.task_labels      (task_id, label_id)
wf_scheduling.workflow_states  (id, team_id, name, category, position)
wf_scheduling.task_comments    (id, task_id, author_user_id, body_md, created_at)
wf_scheduling.task_history     (id, task_id, actor_user_id, kind, from_val, to_val, at)

-- Views + templates (P1)
wf_scheduling.views            (id, subscriber_id, owner_user_id, name, query_json, shared bool)
wf_scheduling.templates        (id, subscriber_id, kind, name, payload_json)

-- Intake (P1)
wf_scheduling.triage_inbox     (id, subscriber_id, source, raw_payload, task_id NULL, state, created_at)
wf_scheduling.notifications    (id, user_id, kind, ref_table, ref_id, seen_at, created_at)

-- Automations (P2)
wf_scheduling.automations      (id, team_id, trigger_json, action_json, enabled)

-- =========================================================
-- WORKFORCE-ONLY EXTRAS  (§4)
-- =========================================================
wf_scheduling.task_estimates   (id, task_id,
                                ai_proposed_hours numeric, ai_reasoning text, ai_proposed_at,
                                engineer_hours numeric, engineer_reason text, engineer_signed_at,
                                manager_hours numeric, manager_approved_by uuid, manager_approved_at,
                                round smallint, escalated_at NULL,
                                final_hours numeric,     -- populated once all 3 sign
                                CHECK (final_hours IS NULL OR
                                       (engineer_signed_at IS NOT NULL
                                        AND manager_approved_at IS NOT NULL)))
wf_scheduling.task_actuals     (id, task_id, source text,   -- 'timer'|'commit_window'|'adjust'
                                started_at, ended_at, minutes numeric,
                                blocked bool DEFAULT false,
                                commit_sha text NULL, note text NULL)
wf_scheduling.task_blockers    (id, task_id, blocker_task_id NULL, blocker_note, opened_at, cleared_at)
wf_scheduling.diary_rollups    (id, subscriber_id, user_id, period text,  -- 'day'|'week'|'month'
                                period_start date, score numeric,
                                delta_median numeric, active_hours numeric,
                                tasks_done int, breakdown_json, generated_at)
wf_scheduling.access_grants    (id, user_id, kind text,     -- 'ai_sub'|'repo'|'board'|'ci'|'chat'|'scope_signed'
                                scope_json, granted_at, revoked_at NULL,
                                manager_countersigned_at NULL)

§3 · Facet parity table — Linear ↔ Workforce

#Linear facetWorkforce namePriPhaseNotes / mapping
A · Core work (Linear's foundation — all P0)
1TeamTeamP0Ph 11:1. Team key (KL, M3, K1, AO) prefixes task identifiers.
2IssueTask (renamed)P0Ph 1Renamed to "Task" — cross-industry terminology (matches Asana/Monday/Height). Same primitive underneath.
3Sub-issueSubtask (1 level)P0Ph 1Linear allows N levels; we cap at 1 (parent_task_id). Deeper nesting breaks the diary rollup honesty.
4Estimate (points)Required hours CHANGEDP0Ph 1Hours, not points. Signed by 3-way agreement (§4). Points don't produce a fair KPI.
5Workflow stateWorkflow stateP0Ph 1Same 5 core: Backlog / To do / In progress / In review / Done. Custom states per team allowed.
6PriorityPriorityP0Ph 15 levels: No / Urgent / High / Medium / Low. Same UI (bar-icon).
7LabelLabelP0Ph 1Team-scoped. Feeds AI Helper's "which folder does this touch?" inference.
8AssigneeAssignee (exactly 1)P0Ph 1Linear supports multi-assignee — dropped. KPI attribution needs one owner.
9Comments / activityComments + historyP0Ph 1Markdown body. Every state / estimate / assignee change writes to task_history.
B · Time-boxing + grouping (all P0)
10CycleCycleP0Ph 1Weekly by default; per-team override to 1/2/3 weeks. Drives weekly diary rollup.
11ProjectProjectP0Ph 1Multi-cycle initiative. Team-scoped or cross-team.
12MilestoneMilestoneP1Ph 2Checkpoint within a project. Ordered list.
13InitiativeDROPHigher-order project grouping. Out of scope v1 — projects already give enough hierarchy for a 50-engineer team.
C · Planning (P0–P1)
14RoadmapRoadmapP1Ph 2Projects on a timeline. Drag to reschedule. Reuses project + milestone data.
15Views / saved filtersViewsP0Ph 1Query grammar (JSON in views.query_json). Per-user + shared. Linear-compatible syntax.
16TemplatesTemplatesP1Ph 2Task templates + project templates. payload_json is the template body.
D · Intake
17TriageTriageP1Ph 2Inbox for new tasks before assignment. Supports email + webhook sources.
18Customer requestsDROPCustomer-facing intake. Out of scope — subscribers with customer intake use their existing tool (Intercom / Zendesk / Kbot itself for our own).
19Inbox / notificationsNotificationsP0Ph 1Per-user alert queue for @-mentions, state changes on assigned tasks, review requests.
E · Docs
20DocumentsDROPNotion-like pages living next to code. Out of scope — subscribers use their existing wiki (Notion / repo README / CLAUDE.md).
F · Automation
21AutomationsAutomations (basic)P2Ph 3Rule-based triggers (state change → assign, label → route to team). Ship 10 canned rules v1, not a builder.
22SLA rulesDROPTime-to-response gates. Out of scope — R&D use case doesn't need SLA; this is a support-desk feature.
G · Integrations
23GitHub / GitLabGitHub integrationP0Ph 1PR-to-task linking, auto-close on merge, commit refs. Runs on our existing GitHub App.
24SlackZoom (Slack on request)P1Ph 2Different chat surface, same wiring. Slack adapter is a swap-in; not v1.
25Figma / Zapier / etc. (~20 integrations)DROP v1Long tail. Ship webhooks + REST API instead; customers can wire what they need via Zapier themselves.
H · Analytics
26Insights (custom analytics builder)DROPHeavy custom-analytics UI. Out of scope — the Workforce diary + fair-score KPI already answer the questions R&D managers actually ask.
27Cycle / project graphsCycle + project graphsP1Ph 2Built-in charts on cycle burndown, project progress. Not a builder — canned.
I · Platform
28API + webhooksREST + webhooksP0Ph 1REST v1 (not GraphQL — matches rest of Workforce). Webhook per team.
29Keyboard shortcuts (system-wide)Keyboard shortcutsP0Ph 1Mirror Linear's exact shortcuts (C, T, A, N, G+D, ?, /). Muscle-memory preservation is the whole point.
30Changelog (customer-facing)DROPPublic product changelog. Different product category.
J · Workforce-only extras — the 6 things Linear does not have
31— (none)AI-proposed hours NEWP0Ph 1AI Helper proposes required hours with reasoning. Table: task_estimates.ai_*. Deep spec §4.1.
32— (none)Three-way agreement NEWP0Ph 1AI + engineer + manager sign the estimate before task can move to In progress. Deep spec §4.2.
33— (none)Actual-hour tracking NEWP0Ph 1Commit-window inference + optional task-timer. Blocked-time excluded. Deep spec §4.3.
34— (none)Delta score NEWP0Ph 1Delta = actual ÷ required. Bands: <0.9 / 0.9–1.2 / >1.2. Deep spec §4.4.
35— (none)Diary rollup (day / week / month) NEWP1Ph 2Per-user rollup jobs; drives the diary artifact. Deep spec §4.5.
36— (none)T0 access grants NEWP0Ph 1Pre-flight access-grant flow. Task can't be assigned until user has T0 signed. Deep spec §4.6.

§4 · Workforce-only extras — deep spec

§4.1 · AI-proposed hours

Trigger

Fires when a task is created and has non-empty title + description + at least one label. Debounced 15s (edits during creation don't re-fire).

Inputs to the AI Helper

{
  task: { title, description, labels[], team, priority, parent_task_id },
  similar_past_tasks: [top-5 by embedding, from wf_scheduling.tasks WHERE completed_at IS NOT NULL],
  code_paths_touched: [from label inference + repo folder map],
  engineer: { user_id, past_delta_median, familiarity_score_for_labels },
  ai_subscription_history: [engineer's past sessions on similar code paths]
}

Output → written to task_estimates

ai_proposed_hours: numeric
ai_reasoning: text   -- 2-3 sentences, cites similar tasks by ID
ai_proposed_at: timestamptz
round: 1

Constraint

If engineer has no past_delta_median (new hire, first task), AI proposal is flagged tentative and requires 2 rounds minimum before converging — protects new hires from an under-calibrated first estimate.

§4.2 · Three-way agreement

State machine

proposed  →  engineer_countered  →  ai_re-proposed  →  engineer_signed  →  manager_pending  →  manager_approved  →  final_locked

  ↓ any state
escalated (after round 3)  →  manager_arbitrates  →  final_locked

Rules

  • Task cannot transition to state in_progress until final_hours IS NOT NULL.
  • Manager approval SLA: 4 working hours from engineer_signed_at. If breached, notification fires to manager's manager (or Steve if no chain configured).
  • All three sigs (AI, engineer, manager) written with actor + timestamp — immutable audit trail.
  • Re-estimation post-sign: creates a new task_estimates row with round++; old row preserved. Requires the same 3-way sign to activate.

API

POST /api/wf/tasks/{id}/estimate/propose        { ai_hours, reasoning }
POST /api/wf/tasks/{id}/estimate/counter         { hours, reason }
POST /api/wf/tasks/{id}/estimate/engineer-sign
POST /api/wf/tasks/{id}/estimate/manager-approve
POST /api/wf/tasks/{id}/estimate/escalate

§4.3 · Actual-hour tracking

Two sources, additive + deduped

Source A · Commit-window inference. Any commit whose branch name, PR link, or commit message references {team_key}-{n} (e.g. KL-152) contributes to that task's actuals. Consecutive commits on the same branch within 60 min = one continuous work window; gap >60 min = new window. Windows stored in task_actuals with source='commit_window'.

Source B · Task-timer. Engineer hits T to start / T to pause. Rows written with source='timer'.

Dedup. When both sources overlap, keep the union of the intervals (not the sum). Query view wf_scheduling.task_actuals_dedup handles this.

Blocked-time exclusion

When task_blockers.cleared_at IS NULL, any actuals with started_at BETWEEN opened_at AND now() are marked blocked=true and excluded from delta calc.

Manual adjust

POST /api/wf/tasks/{id}/actuals/adjust { minutes, note } — writes source='adjust'. All adjusts visible to manager on the task detail.

§4.4 · Delta score

Formula

delta = SUM(actuals.minutes WHERE blocked=false) / 60
        ÷
        task_estimates.final_hours

Bands (per CLAUDE.md · fair-score contract)

< 0.90         → beat_estimate    (green — feeds calibration; no praise/blame)
0.90 – 1.20    → on_plan          (green — target band)
> 1.20         → overrun          (yellow — schedule retro with manager)

Score aggregation (weekly)

Per user, per cycle: delta_median of tasks completed. Feeds §4.5 diary rollup. Never surfaces per-task delta as "bad" — the retro is a conversation, not a punishment.

§4.5 · Diary rollup (day / week / month)

Cron

daily   — 23:59 local time per user's timezone
weekly  — Fri 17:00 local time per user's timezone
monthly — last working day of month, 17:00 local time

Job

for each active user:
  fetch tasks completed in period
  compute:
    - delta_median
    - active_hours (from task_actuals_dedup)
    - tasks_done
    - score (weighted per §4.4 + collaboration + code-quality + ...)
    - breakdown_json (per-category contributions)
  INSERT INTO diary_rollups
  emit notification "Your {period} diary is ready"

Access rule

Engineer sees their own diary before manager does — same URL, same time. No hidden manager preview. Enforced in the read handler.

§4.6 · T0 access grants

Blocking rule

A user cannot be assigned to any task in a team until they have access_grants rows with kind IN ('ai_sub','repo','board','ci','chat','scope_signed') AND manager_countersigned_at IS NOT NULL for at least the scoped-repo grant. Enforced at task-assign time.

Revocation

Any grant can be revoked by the user at any time. UPDATE access_grants SET revoked_at=now() WHERE user_id=$1 AND kind=$2. Manager is notified; cannot block.

Auto-expire

Grants marked temp=true (Charles's temp-assist case) auto-revoke at expires_at. Job runs hourly.

§5 · API surface (v1)

Method + pathPurposeAuth
GET /api/wf/teamsList teams for subscriberrequireApiKey
POST /api/wf/teamsCreate teamrequireApiKey · admin
GET /api/wf/teams/{id}/cyclesList cycles (with filter: current, past, upcoming)requireApiKey
POST /api/wf/tasksCreate task (fires §4.1 AI propose async)requireApiKey
GET /api/wf/tasks/{id}Task detail incl. estimate state + actuals + subtasksrequireApiKey
PATCH /api/wf/tasks/{id}Update fields (state, assignee, priority, labels, ...)requireApiKey
POST /api/wf/tasks/{id}/estimate/*Propose / counter / sign / approve / escalate (§4.2)requireApiKey
POST /api/wf/tasks/{id}/actuals/timerStart/pause task-timerrequireApiKey
POST /api/wf/tasks/{id}/actuals/adjustManual adjust with noterequireApiKey
POST /api/wf/tasks/{id}/blockersOpen/close blockerrequireApiKey
GET /api/wf/viewsList views (personal + shared)requireApiKey
POST /api/wf/viewsCreate view with query_jsonrequireApiKey
GET /api/wf/diary/{user_id}?period=week&period_start=YYYY-MM-DDDiary rollup (user's own OR user's manager)requireApiKey · owner-or-manager
POST /api/wf/access-grantsGrant scoped access (T0 flow)requireApiKey · user-self
DELETE /api/wf/access-grants/{id}Revoke grant (soft — sets revoked_at)requireApiKey · user-self
POST /api/wf/webhooksRegister outbound webhook per teamrequireApiKey · admin

Auth reminder (per CLAUDE.md). Every owner-facing endpoint gates on requireApiKey. Never trust subscriber_id as auth. Diary endpoint additionally checks owner-or-manager via a manages(current_user_id, target_user_id, subscriber_id) helper.

§6 · Phasing — what Codex ships in what order

Phase 1Week 1–2
Core work + AI-proposed + 3-way agreement + actuals + delta + T0. Everything P0. All 9 core-work facets, cycles, projects, views, notifications, GitHub integration, REST + webhooks, keyboard shortcuts, and all 4 blocking Workforce extras. By end of Phase 1: a Kbot-Link engineer can sign T0, get an AI proposal, converge with manager, do the work, watch actual grow, mark done, see delta.
Ship gate Charles + Romit each complete one full task end-to-end without a workaround.
Phase 2Week 3
Diary rollups + planning + intake. Milestones, roadmap, templates, triage, cycle graphs. Diary jobs (day/week/month) run on real data. Zoom integration.
Ship gate First weekly diary generated for both sample subscribers with real scores, not mocks.
Phase 3Week 4
Automation + Linear import. 10 canned automation rules. One-shot Linear workspace import with 14-day read-only safety sync. Slack adapter if requested by first paying subscriber.
Ship gate Linear-workspace import round-trips a real 500-task workspace with zero data loss on sub-issues, labels, comments, history.

§7 · Test contracts + acceptance

Per CLAUDE.md · Bug-class regression guards

  • Rule 1 · every new endpoint ships with 3 valid + 3 malformed + 1 auth-bypass test.
  • Rule 2 · every SQL query with NULL-capable or shared params carries explicit type casts.
  • Rule 3 · every PR adds ≥1 real-postgres integration test (not jest-mocked).
  • Rule 4 · "verified" claim requires a real drivable artifact — TDD test, integration test, Playwright against prod, or curl transcript.

Acceptance for 3-way agreement (Codex's canary)

1. Create task with title + description + label
2. Assert ai_proposed_hours populated within 15s
3. Counter with different hours + reason
4. Assert ai_re-proposed
5. Engineer sign
6. Assert task state cannot change to in_progress
7. Manager approve
8. Assert task state CAN now change to in_progress
9. Skip signature (bypass attempt) → assert 409 CONFLICT with clear error

§8 · Explicitly out of scope (with rationale)

Not buildingRationaleAlternative
Initiatives (higher-order project grouping)Projects give enough hierarchy for a 50-eng team. Adds complexity for the top 10%.Use labels + views.
Customer requests (public intake)Different problem space. Support-desk feature.Intercom / Zendesk / Kbot's own inbox.
Documents (Notion-like pages)Wiki is a different product. Would bloat scope.Notion / repo README / CLAUDE.md.
Insights (custom analytics builder)Diary + fair-score already answers R&D manager questions. Builder-UI adds 6+ months.Canned cycle + project graphs cover 80%.
Sub-sub-issues (N-level nesting)Breaks diary rollup honesty and encourages over-decomposition.One-level nesting + subtask templates for common patterns.
Multi-assigneeKPI needs one owner. Multi-assignee breaks attribution.Primary assignee + reviewers/collaborators as a separate field.
SLA rulesSupport-desk feature.Priority + due-date alerts cover urgent cases.
Public customer changelogMarketing tool. Different product.Ship via existing Kbot release-note flow.
Permanent two-way Linear coexistenceDefeats the "cancel Linear" outcome. Splits attention across two tools.One-shot import + 14-day safety net only.
R&D AI Helper · Facet Parity Spec v0.4 · Linear (~30 facets) → Workforce (22 built + 6 Workforce-only) · 8 facets explicitly dropped with rationale · 3 phases across 4 weeks · build-ready for Codex. Companion docs: metric config (32 rows), projects view (Charles + Romit sample), newcomer training (Priya week 1), Linear-format screenshots (cycle view + task detail). All part of R&D AI Helper Profile v0.4.