feat: workspace rules — proposal card, owner review, approved rules in every session

SA
19m ago
+3924-286

Issue for this PR

Replaces #1438 (lesson sync), which is closed in favour of this redesign. Server side: AltimateAI/altimate-backend#7514 (/datamates/{id}/rules) and AltimateAI/altimate-frontend#3603 (the workspace Rules tab).

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

In a project linked to a workspace, what the person teaches the agent can become a team rule. They confirm it on a card, the workspace owner reviews it, and approved rules reach every teammate's sessions. This replaces #1438's background lesson sync, which sent staged lessons without asking.

Proposal card, built once in the session server (src/altimate/rules/proposal.ts, events.ts). It uses the same ask/reply shape as question: rule.proposal.asked goes out on /event, and the client answers with POST /rule-proposal/:id/reply. The TUI renders it now (plugin/tui/altimate/rules.tsx); the IDE extension can render the same event later with no logic of its own.

  • Enter sends the rule to the owner's queue; e edits it (re-linted); n ("Not a rule") suppresses the topic for 30 days.
  • The owner also sees a to approve at once, which becomes "approve anyway" when the rule weakens verification.
  • An unlinked project gets "Save for this project" instead.

When a card appears (trigger.ts), checked after each turn:

  • a topic corrected in 2 or more sessions with no rule covering it (from recurrence's unmatched topics);
  • a correction on the subject of a team rule the session was shown (an edit or retire card, with the current text struck through);
  • the person asks to remember a rule: training_save with kind: "rule" and project scope now shows the card instead of writing training.

One model call (the reflector, scoped to the triggering corrections) words the rule. If the drafter says the rule already covers it (HELPFUL), no card appears.

Rule or memory (classify.ts). A preference ("I prefer…") or a fact ("our warehouse is…") is memory, saved without review: training_save sends the agent to altimate_memory_write. When the wording says neither, the card asks "Save as: Memory · Team rule · Skip".

Sync (sync.ts, service.ts, store.ts):

  • Pull happens at session start (bounded wait), every 5 minutes, and on Sync now. It brings the approved rules, the settings, and review notices for the person's own proposals ("approved", or "rejected" with the reason). The notices show as TUI toasts.
  • Delivery: approved rules are mirrored into a read-only, git-ignored team store (.altimate-code/learn/team/), so the existing delivery, recurrence and learn show treat them like any lesson. They reach the prompt's "Team rules" with their applies_to globs as path triggers.
  • Outbox: proposals are queued offline-safe and sent on the next run.
  • Usage: applied (sessions a rule was delivered in) and corrected again (recurrence after exposure) are reported back to the workspace.

Learning from sessions. The person's own setting wins (learn.capture, ALTIMATE_LEARN_CAPTURE, or /rules → Turn learning on/off); otherwise a linked project follows the workspace's "Learn from sessions". startCapture takes force so the rules service can start capture on the workspace's say-so.

Also:

  • Server routes GET /rule-proposal, GET /altimate/rules/summary, GET /altimate/rules and POST /altimate/rules/sync.
  • The TUI gets /rules and a sidebar rules: line.
  • learn sync is new, and learn status shows a rules block with the last proposal check.
  • In a linked project, learn bootstrap and learn import-reviews send what they find for review instead of staging a local candidate.
  • api-client errors now carry the coded detail.
  • Docs: "Workspace rules" in configure/learn.md.

Not in this PR:

  • the one-time card offering to bootstrap from history in the background (learn bootstrap is the path today);
  • helpful/harmful usage counts (sent as 0);
  • the IDE extension's renderer.

Beyond the backend contract

This PR also adds, beyond syncing rules with the workspace:

  • the inline proposed-rule card in the TUI (ctrl+x p), and /rules (team rules, your proposals, Sync now, your own learning on/off);
  • a per-person learning setting (POST /altimate/rules/learning, stored in the git-ignored rules state);
  • learn bootstrap / learn import-reviews offering their staged rules to the owner's review, with a confirmation that names the workspace (--send-for-review for scripts);
  • training_save in a linked project proposing a team rule instead of saving project training (only when rules are enabled);
  • the rule-or-memory classifier for "remember …" requests.

Not in this PR (follow-up): showing cards where no TUI renders them — altimate-code run, the IDE extension, ACP, and the Claude Code / Codex plugins.

How did you verify your code works?

  • bun run typecheck.
  • test/altimate/rules/rules.test.ts (20 tests): classification, applies_to, learning precedence, the team mirror, the summary line, card actions per project, edit lint and the verification flag, and dismissal.
  • Existing learn, carry-forward and server suites: 2438 pass. The httpapi-experimental / httpapi-mcp timeouts fail on a clean main too.
  • End to end, with the TUI in tmux against a local backend with #7514 and the Rules tab from #3603:
    • "remember this as a team rule" → card → sent → owner approves in the web → the next session receives it, and the agent cited it;
    • the same correction in 2 sessions → the card appears on its own with "seen in 2 sessions" and the quote;
    • editing the wording;
    • rejecting with a reason → a toast with the reason;
    • owner Approve now, and Approve anyway for "skip dbt tests…";
    • "I prefer CTEs" → saved to memory, no card;
    • usage counts visible in the web.

Cost side effects

  • Drafting: one model call per proposal check that finds a candidate, at most one per turn, and only after a correction (never for turns without one). It runs with the configured learn.model or the session's model.
  • HTTP: one request to the workspace every 5 minutes per open project, plus one per sent proposal and per usage batch. Sizes are bounded (≤50 proposals and ≤500 usage items per request).

Screenshots / recordings

"Remember this as a team rule" → card (owner view: Send, Approve now, Edit, Not a rule)
card

Corrected in 2 sessions → card on its own, with the evidence and the correction quote
recurrence

After e (edit wording)
edit

A rule that weakens verification: flagged, "Approve anyway"; the wording was unclear, so it also offers Memory
flagged

Rejection notice with the owner's reason
rejected

/rules after approval: team rules, your proposals, Sync now, learning on/off
menu

"I prefer CTEs" → saved to memory, no card
memory

The same rules in the workspace's Rules tab (#3603)
web

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR
  • I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Linked projects can sync workspace rules, submit proposals from corrections or explicit requests, and report rule usage.
    • Use /rules to browse team rules and proposals, manage personal learning settings, and sync. The learn sync command is also available.
    • Proposals can be edited, dismissed, or saved locally or as memories. Workspace owners can review and approve proposals.
    • The workspace sidebar displays rule status and the last sync time.
    • Imported or bootstrapped rules can be submitted for workspace review with consent.
  • Improvements

    • Project-level learning settings are supported. Capture and reflection are off by default; approved workspace rules are still delivered unless learning is disabled.
    • Learning status shows where its setting comes from, and documentation and CLI terminology now consistently refer to rules.