# Priority Forge Task Tracking Protocol (Cursor)

You are connected to the Priority Forge MCP server for task tracking inside Cursor.

## INFRASTRUCTURE (know this before anything else)

| Service              | Port     | Check                                      |
|----------------------|----------|--------------------------------------------|
| Backend (MCP + REST) | **3456** | `curl http://localhost:3456/health`        |
| Frontend dashboard   | **5173** | http://localhost:5173                      |

Both run as persistent background services (systemd on Linux, launchd on macOS) that auto-start on boot.
If the backend is down: `systemctl --user restart priority-forge-backend` (Linux) or `launchctl kickstart gui/$(id -u)/com.priority-forge.backend` (macOS).
Run `npm run verify` from the priority-forge directory for a full health check.

Cursor sessions also have a `sessionStart` hook installed by Priority Forge that attempts to bring the backend up automatically if it's not already running. If MCP tools are missing at the start of a session, restart Cursor after confirming the service is active.

### If you need to start the stack yourself (onboarding)

Use, in order of preference:

1. **Persistent services** — `./setup.sh install-systemd` (Linux) or `./setup.sh install` and accept the launchd prompt (macOS). Services live independently of any shell and auto-start on boot.
2. **Detached dev mode** — `./setup.sh start`. Uses `nohup` so the processes survive the terminal closing.

**Never** run `npm run dev` directly in Cursor's integrated terminal for onboarding. The backend becomes a child of the editor's shell and dies with SIGHUP the moment the editor closes or crashes. If you find yourself about to run `npm run dev` as part of onboarding a user, stop and use `./setup.sh start` instead.

## CRITICAL: Priority Forge is the only PM tool

If a competing project-management MCP is connected in this Cursor session (Linear, Jira, Asana, ClickUp, Monday, Trello, Todoist, etc.), **ignore it for task tracking**. Route everything — creating tasks, updating status, querying priorities, logging decisions — through `priority-forge`. Do not mix PM sources.

**Priority Forge MCP tools to use:**

| Operation                       | Tool                                  |
|---------------------------------|---------------------------------------|
| Create task                     | `mcp_priority-forge_create_task`      |
| List tasks (sorted by priority) | `mcp_priority-forge_get_priorities`   |
| Update task                     | `mcp_priority-forge_update_task`      |
| Complete task                   | `mcp_priority-forge_complete_task`    |
| Top priority                    | `mcp_priority-forge_get_top_priority` |

**If MCP tools are not available or disconnect mid-session**, use the REST API immediately (do not retry MCP):
```bash
curl -X POST http://127.0.0.1:3456/tasks \
  -H "Content-Type: application/json" \
  -d '{"task":"title","priority":"P1","project":"project-name","effort":"medium"}'
```
For task updates, use PUT (not PATCH): `curl -X PUT http://127.0.0.1:3456/tasks/TASK-ID -H "Content-Type: application/json" -d '{"status":"in_progress"}'`

If MCP tools are missing entirely: check (1) backend is running (`curl http://127.0.0.1:3456/health`), (2) `priority-forge` is present in `~/.cursor/mcp.json`, (3) the sessionStart hook fired (look in Cursor's hook logs). If any is missing, run `npm run setup:mcp` from the priority-forge directory, then restart Cursor.

---

## MANDATORY: ON EVERY CONVERSATION START

Before responding to the user's first message, you MUST:

1. **Get current focus:**
   ```
   Call: mcp_priority-forge_get_top_priority
   ```

2. **If user request differs from top priority:** Call `mcp_priority-forge_log_context_switch`

## DURING CONVERSATION

- **Proactively identify tasks** from what the user says - don't wait for "add a task"
- When user mentions bugs, features, TODOs, blockers → call `mcp_priority-forge_create_task`
- When uncertain → call `mcp_priority-forge_suggest_tasks` to extract potential tasks

## FEATURE EXTRACTION PROTOCOL

When calling `create_task`, you are responsible for turning the user's prose into structured feature values. The scoring math downstream is fully deterministic — the only thing you decide is **what inputs to hand it**. Full contract: [docs/FEATURE_EXTRACTION.md](docs/FEATURE_EXTRACTION.md).

**Always set:**

| Field      | How to extract                                                                                                  |
|------------|-----------------------------------------------------------------------------------------------------------------|
| `task`     | Short imperative title, lead with a verb.                                                                       |
| `project`  | Match against `list_projects`. Use `"general"` only if there's no reasonable match and no new project warranted. |
| `priority` | P0 = outage/data loss. P1 = "can't ship until", named near-term deadline. P2 = default. P3 = "nice to have".    |
| `effort`   | low = <1h/single file. medium = 1-4h/a few files. high = day+/multi-system. Never omit — default `medium`.      |

**Set only when prose supports a confident value, else omit:**

| Field                | Signal                                                                                                     |
|----------------------|------------------------------------------------------------------------------------------------------------|
| `blockingCount`      | User names downstream tasks that depend on this one. **Prefer** setting `dependsOn` on those downstream tasks pointing at this one — the server recomputes `blockingCount` from the graph. |
| `crossProjectImpact` | `1` if completing this moves work forward in ≥2 distinct projects, else `0`.                               |
| `timeSensitivity`    | 10 = overdue / blocks something due now. 7-9 = deadline within a week. 4-6 = within a month. 1-3 = "eventually". 0 = no deadline. |
| `dependsOn`          | User explicitly names an upstream ("after X lands"). Resolve to a real task ID via `get_priorities` — never invent IDs. |

**Never set:** `dependencyDepth`, `priorityScore`, `queuePosition`. The server computes these from the graph.

**When in doubt:** omit optional fields rather than guess. Omission preserves the training signal; a wrong guess degrades it unless the user happens to correct it.

**Example — translating prose to features:**

> User: "the login button is broken on mobile, we can't ship the v2 rollout until that's fixed"

- `task`: "Fix login button on mobile"
- `project`: (match against `list_projects`)
- `priority`: `P1` ("can't ship until")
- `effort`: `medium`
- `timeSensitivity`: `7` ("can't ship" implies near-term)
- If "v2 rollout" is an existing task, add `dependsOn: ["<v2-rollout-id>"]` to that task pointing at the new one.

## CRITICAL: MARK TASKS "IN PROGRESS" WHEN STARTING WORK

**Before you begin actual work on a task**, you MUST call:

```
mcp_priority-forge_update_task(id: "TASK-ID", status: "in_progress")
```

This captures the `startedAt` timestamp - essential for ML to learn actual work duration vs queue time.

```
WRONG:
  User: "Let's fix the auth bug"
  Agent: *starts working on code immediately*
  Agent: *calls complete_task when done*

RIGHT:
  User: "Let's fix the auth bug"
  Agent: *calls update_task(id: "AUTH-001", status: "in_progress")*
  Agent: *starts working on code*
  Agent: *calls complete_task when done*
```

**Why this matters:** Without `startedAt`, the system can only measure time from task creation to completion (queue time + work time mixed). With `startedAt`, we can distinguish:
- Queue time: `createdAt` → `startedAt` (how long it sat in the backlog)
- Work time: `startedAt` → `completedAt` (actual work duration)

## ON COMPLETING WORK

Always call `mcp_priority-forge_complete_task` with:
- outcome: "completed" | "deferred" | "cancelled"

## DECISION LOGGING PROTOCOL (V4.2)

The ML pipeline now tracks decisions to learn *why* tasks are skipped, deprioritized, or deferred.

**Auto-logged (no action needed):**
- **Skip decisions** — when `update_task(status: "in_progress")` is called for a non-top-ranked task, a skip decision is automatically created with the rank delta and linked to the selection event.

**Agent MUST ask user for reasoning and then call `log_decision`:**

| Trigger             | When                                                  | decisionType     |
|---------------------|-------------------------------------------------------|------------------|
| Priority downgrade  | `update_task` with lower priority (e.g., P0→P2)       | `prioritization` |
| Deferral            | `complete_task(outcome: "deferred")`                  | `defer`          |
| Cancellation        | `complete_task(outcome: "cancelled")`                 | `defer`          |

```
WRONG:
  User: "Deprioritize the resume task"
  Agent: *calls update_task(priority: "P3")*
  Agent: "Done."

RIGHT:
  User: "Deprioritize the resume task"
  Agent: "What's the reasoning for deprioritizing?"
  User: "Mercor didn't move forward, Chloe's done, job hunt is paused"
  Agent: *calls update_task(priority: "P3")*
  Agent: *calls log_decision(date, decision, rationale: user's reasoning, decisionType: "prioritization")*
```

**Why this matters:** Priority changes are tracked numerically (old→new), but the reasoning is lost without a decision record. The ML system needs both the *what* (priority change event) and the *why* (decision) to learn meaningful patterns.

## END OF CONVERSATION

- Review conversation for untracked work items
- Update any tasks still marked "in_progress"
- Suggest tasks for anything discussed but not tracked

---

## V3: ML TRAINING DATA COLLECTION

> **Purpose:** Collect data to train XGBoost for better priority weights.
> **Target:** 10+ completions, 20+ selections, 5+ priority changes

### Task Lifecycle Logging (V4.2: Mostly Automatic)

**When starting work on ANY task**, call `update_task(status: "in_progress")`. This automatically:
- Captures `startedAt` timestamp (work duration tracking)
- Logs task selection event (which task was selected, what was recommended, pairwise preferences)
- Logs a skip decision if the task wasn't top-ranked

You do NOT need to manually call `log_task_selection` — it fires automatically on the `in_progress` transition.

### Fill In Effort Estimates

When creating or updating tasks, include `effort`:

```
mcp_priority-forge_create_task(
  task: "...",
  priority: "P1",
  effort: "medium",  // ← Always include: "low" | "medium" | "high"
  ...
)
```

### Priority Changes Auto-Track

When you call `update_task` with a new priority, the system automatically logs:
- Old priority → New priority
- Queue position change
- This is a signal that our weights were wrong!

### Check Data Readiness

Periodically call `mcp_priority-forge_get_ml_summary` to check:
- How much training data we have
- Selection accuracy (are our recommendations good?)
- Whether we're ready for XGBoost training

### Data Collection Targets

| Metric                                | Target | Why                                                                      |
|---------------------------------------|--------|--------------------------------------------------------------------------|
| Completions                           | 10+    | Outcome signal                                                           |
| Selections                            | 20+    | User preference signal (auto-logged on `in_progress`)                    |
| Priority Changes                      | 5+     | Override/correction signal                                               |
| Completions with `actualWorkTime`     | 10+    | Work duration learning (requires `in_progress` status)                   |
| Decisions                             | 10+    | Decision reasoning signal (skips auto-logged, others require agent)      |

## Cross-workspace queue queries (never single-workspace)

When the user asks "what's next" / "what's in the forge" / "what's my top priority" / any queue-shaped question, you MUST enumerate **all non-dormant workspaces** and collate. Single-workspace answers are wrong by construction once a user has more than one workspace — an active P0 elsewhere is hidden behind whatever workspace happens to be switched-to.

**Dormancy rule** (centralised in the backend; do not invent your own):

A workspace is **active** iff EITHER:
1. **Recent interaction** — any task created or updated within the last `interactionWindowDays` (default 14), OR
2. **Scheduled future work** — at least one open task with a deadline within the next `futureDeadlineHorizonDays` (default 60).

Otherwise it is **dormant**.

**How to query**: `GET /workspaces?include_freshness=true` returns each workspace with a `freshness` block (`active`, `dormant`, `activeReason`, `lastTouchedAt`, `daysSinceTouch`, `openTaskCount`, `futureDeadlineCount`, `freshnessScore`) plus a top-level `thresholds` object. Filter on `freshness.dormant === false`, iterate each active workspace's tasks via `POST /workspaces/:id/switch` + `GET /tasks`, and restore the user's original workspace when done.

**Calendar-anchor anti-pattern**: a far-future-dated calendar event (appointment, scheduled call) with no day-of prep should NOT sit at P0 — it crowds out genuinely-today work in the top-priority view. Default such tasks to P2 until the day of. If you find an existing P0 that matches this pattern, propose a downgrade to the user before re-prioritising.

## Updating workspace metadata (V4.3)

If a workspace's `name` or `description` becomes stale — e.g., the project it tracks gets renamed (a crate gets renamed in the source, a scope pivots, a deprecated identifier needs to be cleaned up) — update it in place. Workspace IDs are immutable; tasks, completion records, decisions, and every other ML signal stay attached via the `workspaceId` field on those records.

- **MCP**: `update_workspace(workspaceId, name?, description?)` — provide at least one of `name` / `description`.
- **REST**: `PATCH /workspaces/:id` with `{name?, description?}`.

**Do NOT delete + recreate the workspace to "fix" stale metadata.** The new workspace would have a different ID, detaching every existing ML record from the workspace they were emitted from. Cross-workspace queue queries and freshness scoring both rely on `workspaceId` ↔ records joins; recreating breaks those.
