fix(rules): a project linked by path can gain its git remote, and `learn status` says why rules are off

Merged
SA
3d ago
+133-6

Closes #1663.

Problem

Workspace rules are keyed by the link's git remote (rules/sync.ts, no_remote). In a project linked by path:

  • learn status printed Workspace rules: not linked (run altimate-code link) for a linked project, and that command fixed nothing.
  • learn sync said only "this project has no git remote".
  • After adding a remote, link --workspace <id> --yes stopped early with "Already linked", so the link stayed path-only and rules stayed off. The interactive picker ("Kept … nothing changed") and the TUI picker ("Already linked … nothing changed") did the same.
  • The docs didn't say rules need a remote.

Backend

No change needed. POST /bind (bind_existing in altimate-backend/app/api/bindings/api.py) already handles this. For the workspace the project is already bound to, it stores a remote the binding lacks on the existing row, if the caller owns the binding. The CLI never sent that request.

Changes

  • link (headless + interactive) and the TUI link picker: when you pick the workspace the project is already linked to, and the link was found by path, has no remote, and the project has one now (addsRemoteToLink), the client sends POST /bind again. The link keeps its workspace and gains the remote, and the local record is updated. A link that already has a different remote isn't touched, because changing it is a re-link.
  • learn status: before the first sync, the rules line comes from RulesSync.context. It now tells apart not linked, linked by path only (rules need a git remote), not signed in, learning off, and linked but not synced yet. This only makes network calls when nothing has synced.
  • no_remote skip reason (learn sync, TUI toast): now explains what to do.
  • Docs: new "Rules need a git remote" paragraph in configure/learn.md.

Tests

  • test/cli/workspace-command.test.ts: headless remote fix; cases where addsRemoteToLink should and shouldn't fire.
  • test/altimate/plugin/workspace.test.ts: TUI picker binds again when the link gains a remote, and changes nothing otherwise.
  • test/altimate/rules/rules.test.ts: a path-only binding reaches the new learn status line; the other gate outcomes.

bun test on those three files: 272 pass. bun run typecheck is clean.

Verification

Manual end-to-end check: the CLI ran from this branch's source against a local altimate-backend (development, started with start_server.sh) on the hackdev tenant, with a separate home and a fresh git project that had no remote.

StepResult
link --create gh1663-e2eWorkspace 81 created; workspace status: "Matched by path"
learn statusWorkspace rules: off — this project is linked by its path, and workspace rules need a git remote on the link (add a remote if the project has none, then run altimate-code link)
learn syncRules sync skipped: with the same reason
git remote add origin …, then learn syncStill skipped; the link has no remote yet
link --workspace 81 (no --yes)This project is linked by its path. Adding its git remote to the link, so workspace rules can sync. → Linked to "gh1663-e2e".
GET /datamate-project-bindings/by-remote and /by-pathBoth return the same row (id 16, workspace 81) with repo_remote and project_path set: updated in place
workspace statusMatched by git remote: https://github.com/acme-e2e/gh1663-demo-…
learn syncSynced: 0 team rules, 0 pending review. (exit 0)
learn statusWorkspace rules: gh1663-e2e — 0 team rules; last sync …
link --workspace 81 againNo remote is added; it only confirms the link

The TUI picker was not checked by hand; its tests in test/altimate/plugin/workspace.test.ts cover it.

🤖 Generated with Claude Code