# Priority Forge — Claude Code Rules

## NEVER use Claude's native task tools

Claude Code's built-in TaskCreate, TaskList, TaskUpdate, TaskOutput, TaskStop are **ephemeral and session-only**.
They do NOT sync with Priority Forge, do NOT appear at localhost:5173, and are LOST when the session ends.

**Always use Priority Forge MCP tools:**
- Create tasks: `mcp_priority-forge_create_task`
- List/query: `mcp_priority-forge_get_priorities` or `mcp_priority-forge_get_top_priority`
- Update: `mcp_priority-forge_update_task`
- Complete: `mcp_priority-forge_complete_task`

**If MCP tools are unavailable or disconnect mid-session**, use the REST API immediately:
```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"}'
```

## Infrastructure

- Backend: systemd user service `priority-forge-backend` — runs on port 3456, auto-starts on boot
- Frontend: systemd user service `priority-forge-frontend` — runs on port 5173
- MCP config:
  - Shared project scope: repo-root `.mcp.json`
  - Personal local scope: `~/.claude.json → projects[cwd].mcpServers` via `claude mcp add --transport http --scope local priority-forge http://127.0.0.1:3456/mcp`
  - Project-scoped `.mcp.json` servers prompt once for approval on first use
  - **NOT** `--scope user` — top-level `mcpServers` is written but not what we rely on for Claude Code sessions
- Agent rules: `~/.claude/CLAUDE.md` (this pattern; NOT AGENTS.md)

Check backend: `curl http://localhost:3456/health`
Check service: `systemctl --user status priority-forge-backend`

## Starting the stack (onboarding)

If the backend/frontend aren't running and you need to start them yourself:

1. **Preferred:** `./setup.sh install-systemd` (Linux) or `./setup.sh install` on macOS and accept the launchd prompt — persistent services survive shell/editor death and reboots.
2. **Otherwise:** `./setup.sh start` — uses `nohup` to detach from the terminal.

**Never** run `npm run dev` in an editor's integrated terminal (VS Code, Cursor, etc.). The backend becomes a child of the editor's shell and dies with SIGHUP when the editor closes. This is the #1 onboarding failure mode.

## Task creation fields

```json
{
  "task": "title",
  "priority": "P0|P1|P2|P3",
  "project": "project-name",
  "effort": "low|medium|high",
  "notes": "optional detail"
}
```

Projects: `personal-ops`, `job-hunt`, `builds`, `priority-forge` (or `example-project`)

## V4.2 — synthesized completion safety net

If you call `complete_task` on a task that was never moved to `in_progress`, the storage layer auto-synthesizes the missing transition: `startedAt = createdAt`, fires a `TaskSelectionEvent` (which auto-creates skip decisions for higher-ranked tasks), and marks the completion record with `synthesizedSelection: true` so trainers can filter or down-weight the artificial work-window.

This is **a safety net for retroactive logging**, not a license to skip the lifecycle. The proper pattern when actually starting work:

1. `update_task(id, status: "in_progress")` ← captures real `startedAt`
2. ...do the work...
3. `complete_task(id, outcome: "completed")` ← clean record

Only fall back to direct `complete_task` when logging work that already happened (so you don't have a meaningful `startedAt` to capture anyway). The synthesized selection event still fires, so the skip-decision data is preserved either way.

## Hub boundary

The `priority-forge-hub` package (at `packages/priority-forge-hub/`) stores **cross-forge identity, observations, and audit only**. It never stores:

- **Task content** — title, priority, effort, notes, body, deadline. These live in PF.
- **Learning signals** — task selections, skip decisions, priority changes, work durations, heuristic weights. These are PF-local ML inputs and must not flow into the hub.

When extending the hub with new event types or `SharedTask` fields, ask whether the new shape carries task content or learning signals. If yes, the field belongs in PF — synthesize a hub-side observation (e.g. `task_updated` with a metadata-only payload) rather than leaking the field directly. The schema-level enforcement: `MinimalTask` in `packages/priority-forge-hub/src/types.ts` is intentionally narrow (`id`, `status`, `dependencies?`, `blocking?`, `claimedBy?`). Don't widen it.

This rule is the single load-bearing guardrail that prevents the hub from absorbing PF's roles (ranking, lifecycle tracking, ML training).

## Cross-workspace queue queries — never single-workspace

When a user asks "what's next", "what's in the forge", "what's my top priority", or anything queue-shaped, you MUST enumerate **all non-dormant workspaces** and collate, not just the currently-active one. Single-workspace answers are wrong by construction once a user has more than one workspace: an active P0 elsewhere will be 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**: hit `GET /workspaces?include_freshness=true`. Each workspace gets a `freshness` block with `active`, `dormant`, `activeReason`, plus a `freshnessScore` (decayed sum of interaction timestamps, τ = 7d by default) for ranking active workspaces against each other. The top-level `thresholds` object documents the cutoffs in effect.

Filter on `freshness.dormant === false`, then iterate each active workspace's tasks via `POST /workspaces/:id/switch` + `GET /tasks` (or pass through the MCP equivalents). Restore the user's original workspace when done.

**Calendar-anchor P0 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, then bump to P0. 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.
