Custos turns the bulk-upload pile into a triage desk

Drop a stack of contracts in and the fork reads each one before you've filed anything.

intakeworkflow

Custos has built an intake queue for legal-os: when you bulk-upload documents, they don't vanish into a folder you have to organise by hand. Each one gets read once by an AI pass that guesses the counterparty, whether it's a buyer- or seller-side paper, where it sits in the contract lifecycle (an original, an amendment, an NDA, a statement of work), and how sure it is. Unfiled documents now show up as first-class objects rather than orphans. The triage row offers an "assign to" dropdown, and creating a new home for a document pre-fills a sensible name like "Adobe Inc. - SOW" instead of the raw filename. The customer overview surfaces a running count of loose documents per counterparty, and anything unassigned floats to the top of the timeline with an amber dot so it can't be quietly forgotten.

So what Worth a look for any legal-ops team that ingests contracts in batches and wants the system, not a paralegal, to do the first sort.

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