feat(api): link_job_to_org agent tool (#375 follow-up) #379

Merged
james merged 1 commit from link-job-to-org-tool into main 2026-06-29 17:18:46 +00:00
Owner

The agent tool follow-up from #375. An "earned" domain write tool (ADR-0030) mirroring link_person_to_org, so the built-in agent (and MCP clients) can point a Job/Contract's employer at a tracked Organization.

What's in it

  • link_job_to_org (safe_update, entityType job): input { job_id, organization_id }. A job's employer is a column on the row (not a junction), so this is a scoped update — it sets organization_id and snapshots the org's live name into company. Works for both jobs and contracts (same table).
  • Validation: reuses the shared resolveJobOrganizationLink resolver — a cross-user/missing org id is a 404 (don't leak existence) at both propose and apply; the job ownership check rejects another user's job the same way.
  • No new hole: the generic create_job/update_job tools still don't expose organizationId, so this dedicated, validated tool is the only agent path to link.

Verification

  • typecheck ✓ · lint ✓ · openapi:check ✓ (no REST change — agent tools aren't in the spec) · api-client check ✓ · semgrep ✓ 0 findings.
  • test against both engines (ephemeral Postgres): 1268 passed, 0 skipped — incl. a dual-engine test (link snapshots the org name + resolves the live name on read; cross-user org id and cross-user job both → 404), and the existing registry/MCP tools/list tests still pass with the tool added.

Known limitation (worth a follow-up)

Undoing a link_job_to_org from the activity screen is currently partial: the undo system's generic inverse is update_<entityType> = update_job, whose schema doesn't include organizationId, so the inverse restores company but not the link (the job stays linked, showing the live org name). The clean fix is to add a validated organizationId to update_job/update_contract (which would also make the link first-class CRUD and fully undoable). I scoped that out to keep this focused; happy to file/take it.

Part of #375.

🤖 Generated with Claude Code

The agent tool follow-up from #375. An "earned" domain write tool (ADR-0030) mirroring `link_person_to_org`, so the built-in agent (and MCP clients) can point a Job/Contract's employer at a tracked Organization. ## What's in it - **`link_job_to_org`** (`safe_update`, entityType `job`): input `{ job_id, organization_id }`. A job's employer is a column on the row (not a junction), so this is a scoped update — it sets `organization_id` and snapshots the org's **live name** into `company`. Works for both jobs and contracts (same table). - **Validation**: reuses the shared `resolveJobOrganizationLink` resolver — a cross-user/missing org id is a **404** (don't leak existence) at both propose and apply; the job ownership check rejects another user's job the same way. - **No new hole**: the generic `create_job`/`update_job` tools still don't expose `organizationId`, so this dedicated, validated tool is the only agent path to link. ## Verification - `typecheck` ✓ · `lint` ✓ · `openapi:check` ✓ (no REST change — agent tools aren't in the spec) · `api-client check` ✓ · semgrep ✓ 0 findings. - `test` against **both engines** (ephemeral Postgres): **1268 passed, 0 skipped** — incl. a dual-engine test (link snapshots the org name + resolves the live name on read; cross-user org id and cross-user job both → 404), and the existing registry/MCP `tools/list` tests still pass with the tool added. ## Known limitation (worth a follow-up) Undoing a `link_job_to_org` from the activity screen is currently **partial**: the undo system's generic inverse is `update_<entityType>` = `update_job`, whose schema doesn't include `organizationId`, so the inverse restores `company` but **not** the link (the job stays linked, showing the live org name). The clean fix is to add a validated `organizationId` to `update_job`/`update_contract` (which would also make the link first-class CRUD and fully undoable). I scoped that out to keep this focused; happy to file/take it. Part of #375. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(api): link_job_to_org agent tool (#375 follow-up)
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 5s
PR / OpenAPI (pull_request) Successful in 2m44s
PR / Static analysis (pull_request) Successful in 2m51s
PR / pnpm audit (pull_request) Successful in 3m2s
PR / Client (web export smoke) (pull_request) Successful in 3m19s
PR / OSV-Scanner (pull_request) Successful in 50s
PR / Lint (pull_request) Successful in 3m34s
PR / Typecheck (pull_request) Successful in 4m9s
PR / Build (pull_request) Successful in 4m17s
PR / Package age policy (soft) (pull_request) Successful in 56s
PR / Test (postgres) (pull_request) Successful in 4m31s
Secrets / gitleaks (pull_request) Successful in 1m6s
PR / Test (sqlite) (pull_request) Successful in 4m41s
PR / Trivy (image) (pull_request) Successful in 2m11s
PR / E2E (Playwright) (pull_request) Successful in 5m11s
PR / Coverage (soft) (pull_request) Successful in 2m15s
6b4a512b0f
An "earned" domain write tool (ADR-0030) mirroring link_person_to_org, so
the agent can point a job/contract's employer at a tracked Organization.
A job's employer is a column on the row (not a junction), so this is a
scoped safe_update: it sets organization_id and snapshots the org's live
name into company. Works for both jobs and contracts (same table).

Org ownership is validated through the shared resolveJobOrganizationLink
resolver — a cross-user/missing org id is a 404 (don't leak existence) at
both propose and apply. The generic create_job/update_job tools still do
NOT expose organizationId, so this is the only (validated) agent path to
link, with no cross-user hole.

Dual-engine test: link snapshots the org name + resolves the live name on
read; cross-user org id and cross-user job both → 404.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 79.6% ≥ 50%
Branches 71.5% ⚠️ ≥ 75%
Functions 80.8% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 79.6% ✅ | ≥ 50% | | Branches | 71.5% ⚠️ | ≥ 75% | | Functions | 80.8% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit 76b147b72e into main 2026-06-29 17:18:46 +00:00
james deleted branch link-job-to-org-tool 2026-06-29 17:18:46 +00:00
Sign in to join this conversation.
No description provided.