Custos builds a triage queue for the contract pile

legalos's Custos fork turns bulk contract uploads into a one-click sort-and-route workflow.

intakecontract-review

Anyone who has migrated a contracts folder knows the pain: hundreds of PDFs land in a system with no idea which counterparty they belong to, whether they're the master agreement or an amendment, or which matter they should sit under. Custos's new intake queue runs each uploaded document through an AI pass that guesses the role (NDA, MSA, SOW, order form, renewal), the counterparty, and whether it's an original or an amendment, then surfaces it in a triage list with a confidence score.

From there, one click routes the document to an existing matter - fuzzy-matched and suggested - or spins up a new project pre-named after the counterparty and document type. Documents that don't get assigned still count: they show up against the customer they belong to, so the counterparty view isn't empty just because nobody filed anything yet.

So what If you've ever inherited a chaotic contracts drive, this is the workflow that turns that drive into a structured book of business without a paralegal-week of clicking.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

4 commits from Custos/legalos, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
32b1f25b Intake: bulk upload + auto-classification + assign-or-create flow Custos 2026-05-04 ↗ GitHub
commit body
Adds an /intake triage queue for bulk-uploaded contracts. Each upload is
classified by an LLM and routed to an existing project (counterparty
match) or a new one with a single click.

Schema:
- documents.intake_role / intake_status / intake_counterparty /
  intake_parent_counterparty / intake_lifecycle_hint / intake_confidence /
  intake_analyzed_at.

Backend:
- New lib/intakeAnalysis.ts: maybeAnalyzeIntake() runs one LLM call that
  determines role (buyer/seller/mutual), status (draft/execution/unknown),
  counterparty, parent entity, lifecycle position (original/amendment/
  renewal/addendum/sow/order_form/etc.), and self-reported confidence.
- Hooked into POST /single-documents and POST /projects/:id/documents,
  fire-and-forget alongside contract-fact extraction.
- GET /single-documents/intake returns pending docs + the user's projects
  for client-side counterparty matching.
- POST /single-documents/:id/assign accepts {project_id} OR
  {new_project: {name, template, counterparty, parent_counterparty}}.

Frontend:
- New /intake route with bulk upload button (multi-file).
- Per-doc row shows role + status + lifecycle pills, counterparty +
  parent + confidence, fuzzy-matched existing project suggestions
  (single click to assign), and a "Create new project" button that
  preselects the right template based on detected role.
- AppSidebar gains Intake entry above Projects.
- Polls every 4s while any doc is mid-analysis.
230083fa Projects + Intake UX: surface counterparty and template, fall-through assign Custos 2026-05-04 ↗ GitHub
commit body
Projects list now leads with what matters for contract management:
- Replaced CM / Chats columns with Type (vendor/customer/internal pill)
  and Counterparty (with parent badge if any).
- Files + Reviews kept; Created replaced with Updated (more useful).

Intake row improvements:
- "Create new project" now names the project as
  "Counterparty - LIFECYCLE_HINT" when both are known (e.g.
  "Adobe Inc. - SOW") so multiple contracts with one counterparty stay
  distinguishable.
- New "Assign to..." dropdown lists every existing project so you can
  pick a target even when fuzzy-match doesn't surface it.
69aef399 Standalone docs first-class: bulk assign, role filters, customer index Custos 2026-05-04 ↗ GitHub
commit body
Backend:
- POST /single-documents/bulk-assign accepts {document_ids, project_id}
  or {document_ids, new_project} and atomically reassigns the batch.
- GET /projects/counterparties now folds standalone documents into the
  customer index. Each group gains a standalone_count field; documents
  without an intake_role match the role filter via intake_role.
- GET /projects/counterparties/:name/timeline includes user-owned
  standalone docs whose intake_counterparty matches.

Frontend (/intake):
- Filter chips by role: All / Vendors / Customers / Internal-Other,
  with per-bucket counts.
- Per-row checkboxes plus a select-all checkbox in the header.
- Sticky bulk-action bar (visible when 1+ selected): pick an existing
  project from a dropdown and "Assign", or "Create new project from
  selection" which auto-derives template + counterparty by majority
  vote across the picked docs.
b375d048 Customer index: surface standalone-doc count + timeline entry Custos 2026-05-04 ↗ GitHub
commit body
- CustomersOverview shows '+N standalone' pill on each counterparty row
  when standalone docs exist, plus a footer link "view full timeline"
  in the expanded section.
- Page total now includes a "X standalone docs" tally.
- CounterpartyTimelineView synthesises an "Unassigned" timeline entry
  when standalone docs exist, so visiting /customers/Adobe Inc. with
  no projects but 2 unassigned PDFs actually renders the docs (was
  showing "no projects" empty state).
- Empty-state copy updated for the new world (don't require projects).

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-44.md from inside the repo you want the changes in.

⬇ Download capture-thread-44.md