# Priority Forge — Claude Code Rules

## NEVER use Claude's native todo-tracking tools

Claude Code's built-in `TaskCreate` and `TaskUpdate` are **ephemeral and session-only** todo-tracking tools. 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 instead:**
- 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`

**`TaskStop` / `TaskOutput` / `TaskGet` / `TaskList` are different — they are subagent process-control tools, not todo trackers.** They inspect and interrupt running `Agent` subagents and have NO Priority Forge equivalent. Use them freely; `TaskStop` is the only way to interrupt a premature subagent.

**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, audit, and the minimal projection seeds needed to coordinate shared work** — never anything that lets it absorb PF's roles (ranking, lifecycle tracking, ML training).

**What MAY flow** (observation metadata + projection seeds):

- **Memory plane** (`team_memory_events`): identity, task **IDs**, the task **title** (so the activity feed and `xmem_get_current_focus` stay human-readable), project, status, lifecycle timestamps, dependency/blocker edges, and — for updates — the **names** of the fields that changed. Enforced by the `projectMemoryPayload()` whitelist in `subscriber.ts`, pinned by `__tests__/memory-redaction.test.ts`.
- **Orchestration plane** (`SharedTask`): `title`, `description`, `deadline`, and an advisory `priority` — the description seeded from the publisher's notes; `deadline` and `priority` advisory (the claimer's forge recomputes time-sensitivity and heuristic weights locally, and may override the priority). Frozen at publish as the seed the claimer's forge projects from. The advisory `priority` is a lead/coordination *input*, distinct from the forbidden forge-local priority value + change history below.

**What must NEVER flow** (task content + learning signals):

- **Task content** beyond the title/description/deadline/priority seeds — effort, body, and raw note edits into the memory plane.
- **Learning signals** — task selections, skip decisions, priority *changes* (old→new deltas), work durations (`actualWorkTime`), heuristic weights, priority scores. These are PF-local ML inputs. (An advisory *seed* priority is allowed on `SharedTask`; a forge's *computed* priority and its *change history* are not.)

When extending the hub with new event types or `SharedTask` fields, ask whether the new shape carries content or a learning signal. If yes, it stays in PF — synthesize a metadata-only observation instead (e.g. `task_updated` carries `changedFields` *names*, never the values or the prior task). `MinimalTask` in `packages/priority-forge-hub/src/types.ts` stays narrow (`id`, `status`, `dependencies?`, `blocking?`, `claimedBy?`); don't widen it. Any new memory-plane field must be added to the `projectMemoryPayload()` whitelist **and** the redaction test in the same change.

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.
