# Feature Extraction Protocol

> **Scope:** universal — applies to every agent that talks to Priority Forge (Cursor, Claude Code, Codex, Droid, and any future MCP client).
>
> **Related:** [ML_ARCHITECTURE.md](ML_ARCHITECTURE.md) for the three-layer intelligence chain that surrounds this protocol. [ROADMAP.md](ROADMAP.md) for the planned transition from agent-driven extraction to a learned extractor (V6).

Priority Forge splits "how important is this task?" into two separate problems:

1. **Semantic extraction** — turning free-form prose ("the login button is broken on mobile, we can't ship v2 until that's fixed") into structured feature values. This layer is agent-driven today.
2. **Mathematical scoring** — turning those feature values into a ranked priority score via a fixed weighted formula. This layer is fully deterministic and lives in the server.

This document is the contract for the first half. If you're writing a rule, skill, hook, or MCP client for Priority Forge, this is the source of truth for what agents are expected to do when they call `create_task` and `update_task`.

---

## The deterministic half (not your problem)

Once a task has feature values, the server computes:

```
priorityScore = basePriority - (
  blocking       * blockingCount       +
  crossProject   * crossProjectImpact  +
  timeSensitive  * timeSensitivity     +
  effortValue    * effortValueRatio    +
  dependency     * dependencyDepth
)
```

`basePriority` is a lookup (`P0=0`, `P1=100`, `P2=200`, `P3=300`). The weights are in the heuristic config (`GET /heuristic-weights`). Lower score = higher priority. Dependency-graph changes trigger deterministic rebalancing; no agent decision is involved downstream of task creation.

The agent never picks the score. The agent picks the inputs.

---

## The semantic half (your problem)

When a user describes work in prose, the agent is responsible for producing the following structured fields on the `create_task` call.

### Fields owned by the agent

| Field                 | Type                         | Required    | What it encodes                                                                                                                              |
|-----------------------|------------------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| `task`                | short string                 | yes         | Imperative one-line title ("Fix login button overflow on mobile Safari").                                                                    |
| `project`             | project slug                 | yes         | The project this belongs to. `"general"` if the user's description doesn't map to a known project; agent should create the project if it's clearly a new one. |
| `priority`            | `"P0" \| "P1" \| "P2" \| "P3"` | yes        | Severity / urgency ceiling. Defaults to `P2` when unclear.                                                                                  |
| `effort`              | `"low" \| "medium" \| "high"` | yes         | Size of work. Always include — missing effort degrades the training signal.                                                                  |
| `blockingCount`       | `0..10`                      | if obvious  | Number of *other tasks in the queue* this one unblocks. Zero if none; omit rather than guessing.                                             |
| `crossProjectImpact`  | `0 \| 1`                     | if obvious  | `1` if completing this task moves forward work in ≥2 distinct projects.                                                                       |
| `timeSensitivity`     | `0..10`                      | if mentioned | Deadline pressure. `0` = no deadline, `10` = overdue or blocking something with a deadline.                                                  |
| `dependsOn`           | array of task IDs            | if mentioned | Explicit upstream dependencies. The server computes `dependencyDepth` from this; you do not set depth directly.                             |
| `effortValueRatio`    | `0..9`                       | optional    | Quick-win score. Higher = more value per unit effort. Usually derivable from `effort` + `priority`; let the server default unless the user is explicit. |

### Fields owned by the server (never set these manually)

- `dependencyDepth` — computed from `dependsOn`.
- `priorityScore` — computed from the formula.
- `queuePosition` — derived from score.
- `createdAt`, `startedAt`, `completedAt` — set by lifecycle calls.
- Updated `blockingCount` — recomputed when any task's `dependsOn` changes.

---

## Per-field extraction guide

Each field below shows the prose signal, the value to pick, and the reasoning the agent should expose.

### `task` (title)

- **Signal:** any mention of work to be done.
- **How:** rewrite as a short imperative phrase, lead with a verb.
- **Good:** `"Fix login button overflow on mobile Safari"`.
- **Bad:** `"The user said something about the login button being broken"`.

### `project`

- **Signal:** the conversation context, an explicit mention ("in postit, ..."), or a filename hint (`src/apps/postit/...`).
- **How:** match the project slug case-sensitively against `list_projects`. If no match and the user clearly named a new project, create it via `create_project` first.
- **Default:** `"general"` only if no mapping exists and no new project is warranted.

### `priority`

- **Signal:** urgency language.
- **Heuristic:**
  - `P0` — outage, data loss, security, "blocking my team now".
  - `P1` — "before we ship", "can't ship v2 until", named deadline within a week.
  - `P2` — default. Normal feature, normal bug, non-urgent.
  - `P3` — "nice to have", "someday", "if we have time".
- **When unsure:** `P2`.

### `effort`

- **Signal:** scope words, file counts, prior knowledge of the system.
- **Heuristic:**
  - `low` — single file, single function, < 1 hour of focused work.
  - `medium` — a handful of files, some design work, 1-4 hours.
  - `high` — multi-system, design decisions, full day or more.
- **When unsure:** `medium`. Never omit.

### `blockingCount`

- **Signal:** the user explicitly says "X blocks Y" or lists multiple downstream tasks that depend on this one.
- **How:** count the *existing tasks in the queue* that this task unblocks. If the user names them, look them up via `get_priorities` and set `dependsOn` on those tasks (pointing at the new task) instead of setting `blockingCount` on the new one. The server recomputes `blockingCount` from the graph — that's the ground truth.
- **When unsure:** omit (default `0`).

### `crossProjectImpact`

- **Signal:** the task's description mentions more than one project, or the blocked tasks live in different projects.
- **Heuristic:** `1` if completing this task moves forward work in at least two distinct projects. Else `0`.
- **When unsure:** `0`.

### `timeSensitivity`

- **Signal:** any deadline language.
- **Heuristic:**
  - `10` — overdue, or the thing it blocks is due now.
  - `7-9` — named deadline within a week.
  - `4-6` — named deadline within a month.
  - `1-3` — soft "eventually" language.
  - `0` — no deadline at all.
- **When unsure:** omit (default `0`).

### `dependsOn`

- **Signal:** the user explicitly names an upstream task ("after the DB migration lands, then ...").
- **How:** resolve the named upstream to a task ID via `get_priorities` or `suggest_tasks`. If the upstream doesn't exist yet, create it first, then create the downstream with `dependsOn: [upstream-id]`.
- **When unsure:** omit. Do not invent task IDs.

---

## The feedback loop (why getting it wrong is tolerable)

Every agent-extracted feature value is a guess. The server treats those guesses as hypotheses and records corrections:

| User action                         | Event logged                  | Training signal                                                          |
|-------------------------------------|-------------------------------|--------------------------------------------------------------------------|
| `update_task(priority: ...)` with a different value | `PriorityChangeEvent`         | Agent's severity read was wrong for this shape of input                  |
| `update_task(status: "in_progress")` on a non-top-ranked task | `TaskSelectionEvent` (auto) | Queue order disagreed with user's actual pick — scoring was wrong        |
| User adds missing `dependsOn`       | `QueueRebalanceEvent`         | Agent missed a dependency the user knew about                            |
| User sets a different `effort`      | `PriorityChangeEvent` + completion record diff | Agent's effort estimate was off vs what actually happened                |
| Drag-reorder in the frontend        | `DragReorderEvent`            | Pairwise user preference that disagrees with the current ranking         |

These events feed the ML pipeline ([ML_ARCHITECTURE.md](ML_ARCHITECTURE.md)) which re-tunes the heuristic weights over time. The weights are the only parameters that change; the scoring formula itself stays fixed.

So: agents extract imperfectly, users correct, weights shift, future extraction by the same agent gets scored more accurately even though the extraction quality didn't change. The system compensates for agent noise at the scoring layer rather than trying to eliminate it at the extraction layer.

---

## Why not make extraction deterministic too?

Every feature above could in principle be derived from prose by rules. Let's take them one at a time and see why none of them survive contact with real user input.

- **`blockingCount`** requires resolving references like "v2 rollout" or "the demo" to task IDs. A rule-based resolver works only if users type task IDs, which they never do at capture time — the whole Priority Forge pitch is "capture tasks from prose, not from forms".
- **`timeSensitivity`** is parseable when the user writes `2026-05-01`. In practice users write "before the demo", "blocking the hiring pipeline", "sometime this quarter". Those map to numbers only with world knowledge.
- **`effort`** is context-dependent by construction. "Quick auth fix" is an hour in one codebase, two weeks in another.
- **`crossProjectImpact`** requires a semantic model of what each project is and which tasks fan out across them.
- **`priority`** sits downstream of all the others — if the agent can't extract the inputs, it can't derive a defensible priority either.

The alternative is to force structured input at capture time, like Jira. That friction is precisely why traditional trackers fail: people stop tracking. Priority Forge's premise only works if capture is conversational, and conversational capture requires an LLM in the loop.

---

## Why not train a classifier today?

It's planned — see [ROADMAP.md](ROADMAP.md#v6-learned-feature-extractor). The blockers for doing it right now are:

1. **Cold start.** A new user has zero (prose → features) training pairs. LLMs generalize across domains from their pretraining; a per-user classifier does not.
2. **Explainability at capture time.** An agent can tell the user "I set `blockingCount=3` because you said 'this unblocks the migration, the frontend, and the demo'." A trained model emits three numbers with no rationale, which makes it impossible to correct at the moment of capture.
3. **Training-data quality.** The events the current system logs (`PriorityChangeEvent`, `TaskSelectionEvent`, etc.) tell us *the agent's extraction was off*, not *the correct extraction was X*. We don't have labeled (prose → features) pairs yet; we have (prose → agent's guess → user's correction of the downstream score) triples. The V6 roadmap entry describes how to synthesize the missing labels by re-running extraction against corrected outcomes.
4. **Distribution shift.** Every project uses different language for the same underlying signals. A classifier trained on one user's corpus won't transfer cleanly.

---

## Implementation checkpoints

If you're adding a new MCP client, a new skill, or a new agent rule file, verify that:

- [ ] The agent is told to always set `task`, `project`, `priority`, `effort`.
- [ ] The agent is told to set `blockingCount`, `crossProjectImpact`, `timeSensitivity`, `dependsOn` only when the prose supports a confident value, otherwise omit.
- [ ] The agent is told never to set `dependencyDepth`, `priorityScore`, or any server-computed field directly.
- [ ] The agent is told to prefer setting `dependsOn` on downstream tasks over setting `blockingCount` on upstream tasks, because the server recomputes `blockingCount` from the graph.
- [ ] The agent is told to fall back to `suggest_tasks` when the prose contains multiple potential tasks — do not guess a single one.
- [ ] The client captures user overrides and routes them through `update_task` (not a fresh `create_task`) so the correction is logged as a `PriorityChangeEvent`.

The agent rule files ([AGENT_RULES.md](../AGENT_RULES.md), [AGENT_RULES_CURSOR.md](../AGENT_RULES_CURSOR.md), [AGENT_RULES_CODEX.md](../AGENT_RULES_CODEX.md)) each reference this document. When the contract changes, update this file first and then propagate the summarized cheat-sheet into the three rule sources.
