Drafting, Screening and Expert Answers
Three built-in tools for work that has to be grounded and has a person in the loop: writing a document from what the knowledge base actually says, triaging incoming requirements against a rubric you wrote, and routing a question to whoever owns it so their answer becomes knowledge.
They share a shape. Each is driven by a configuration file you own — a template, a rubric, a roster — because the sections of your document, the criteria you judge by and the people who answer are the most deployment-specific things you have. None of it is in code. And each stops short of the decision a person should make: a draft is not signed off, a duplicate is not merged, an answer is not invented.
All three write into the workspace under the same actor confinement the file tools obey.
document_draft — write a document section by section
A document of a known kind has sections fixed by its template. That stability is the point: each section is gathered, written and confirmed on its own, so a regeneration can skip the ones a person has already approved.
The template
~/.ugent/document_templates/<name>.json (override the directory with UGENT_DOCUMENT_TEMPLATE_DIR).
{
"title": "Product Manual",
"needs_confirmation_label": "[needs confirmation]",
"sections": [
{ "id": "overview", "title": "Overview",
"instruction": "What the product is for and who uses it" },
{ "id": "specs", "title": "Specifications",
"instruction": "Rated voltage, power, dimensions, weight" },
{ "id": "history", "title": "Revision History",
"instruction": "Version and date", "requires_evidence": false }
]
}Section ids must be unique bare identifiers — confirmation keys on them. requires_evidence defaults to true; set it false for a section like a revision history that nothing in the knowledge base can source. needs_confirmation_label is configurable because the product is used in more than one language.
The flow
createa draft from the template.gatherevidence for one section. Retrieval runs as the calling user, so the engine returns only what that person may read.writethe section. Citations are checked against whatgathersupplied for that section — one invented, or carried over from another section's evidence, is removed and reported.lockto confirm a section, orreturnit with a note saying what to change.renderwrites Markdown beside the draft.
status lists what is still outstanding, which is exactly the set a regeneration pass should touch.
What it guarantees
- A section with no verified evidence is labelled, not quietly written. It renders behind the confirmation label until a person vouches for it.
- A confirmed section is never rewritten or re-gathered. It also drops the label, because someone has now taken responsibility for it.
- Only the person who confirmed a section can withdraw that confirmation, by
unlockor byreturn— both clear it, so binding one and not the other would be no protection at all. A withdrawal records who confirmed, who withdrew and how, andstatusshows it: the rewrite that follows renders as an ordinary draft, and without the record nothing would show that different content had once been approved.
What this is not
It is attribution, not human approval. In someone's own session the model acts as them, so the tool cannot tell the author's agent from the author. Treat a confirmation as "recorded against this person", not as "this person was asked".
requirement_screen — triage intake against a rubric
Score incoming requirements the same way every time, and surface the ones a person should look at.
The rubric
~/.ugent/rubrics/<name>.json (override with UGENT_RUBRIC_DIR).
{
"title": "Feature intake",
"intake_schema": "requirement_intake",
"scale_max": 5,
"min_coverage": 0.7,
"criteria": [
{ "id": "value", "title": "Business value",
"description": "The problem solved and who benefits",
"weight": 3,
"levels": ["0: no stated value", "5: several customers asked, with numbers"] },
{ "id": "clarity", "title": "Clarity",
"description": "Does it state the scenario, the input and the expected result",
"weight": 1 }
],
"dedup": { "embedding_floor": 0.82, "lexical_floor": 0.5, "max_suggestions": 3 },
"ledger_scope": "shared"
}levels are score anchors. They are worth writing: without them a model drifts toward the middle of the scale and scores stop being comparable between runs.
intake_schema is optional — a rubric can score free text without extracting fields first.
What submit returns
Screened `r-3f2a91` against `intake`: total 71.7/100, coverage 100%.
- Business value (weight 3): 4/5 -- "two pilot sites asked for offline entry". Named customers, no figures.
- Clarity (weight 1): 3/5 -- "fills the form offline and syncs when back online". Scenario is clear, expected result is not.
Possible duplicate: r-90c4de (embedding 0.87) Offline form entry for field engineers
Needs review: possible duplicate of r-90c4de.
Recorded in screening/intake.jsonl.What it guarantees
- Every score carries a verbatim quote from the requirement, checked against the text with case, width and punctuation folded. A quote that is not there discards the score rather than being trusted.
- The total is computed in code, as a weighted mean over the criteria that could be assessed — never by the model.
- A requirement is flagged for a person when nothing could be assessed, when assessed weight falls below
min_coverage, when extraction failed, or when a duplicate was suggested. - Duplicates are only ever suggested. Two requirements can be near-identical and differ in the one detail that matters — the same feature for a different platform — so a person decides.
Whose submissions it compares against
ledger_scope decides:
caller(the default) — one ledger per person. Duplicate detection then only finds someone repeating themselves, which on a team intake queue is most of what it cannot see.shared— one ledger in the shared area, so every submission is compared against everyone's. This is what a team intake wants.
Switching starts fresh
A rubric switched to shared starts an empty shared ledger. The per-person ledgers already written are not merged into it — under confinement nobody can read another person's folder, so nothing could merge them. Choose the scope when you write the rubric.
Duplicate detection uses embedding cosine when a memory embedding model is configured and both sides have a vector of the same width, and character-bigram overlap otherwise — which works on Chinese, where word-based overlap does not.
expert_desk — ask the person who owns it
When the knowledge base cannot answer something, ask whoever owns that topic once, and write their answer back so the next person gets it from the knowledge base instead of from them.
The roster
experts.toml, discovered and hot-reloaded like cron.toml.
enabled = true
answers_dir = "expert_answers"
[escalation]
min_citations = 1 # ask when an answer has fewer verified citations
on_needs_confirmation = true # ask when a drafted section still needs confirming
[[expert]]
id = "alice" # the actor id, not a display name
name = "Alice Tan"
topics = ["warranty", "保修"]
route = "web:support:room-12" # a channel the interaction broker knows
notify_timeout_secs = 900
[[expert]]
id = "bob"
name = "Bob Lee"
fallback = true # receives what no topic matchedenabled is off by default, so a stray file in a cloned repository does not start routing questions at people. Topics are matched as substrings, case folded — not as words, because Chinese has no word boundaries to split on. More than one expert can be a fallback; a question nobody owns goes to all of them rather than to whoever happened to be first in the file.
The escalation triggers are all things the tool can check for itself. Nothing keys on the model's own sense of confidence, which is not evidence.
The flow
route— who owns this. No side effects, so the agent can check before deciding to bother anyone.textResponsible for this question: - Alice Tan (`alice`), topics: warranty, reachable on web:support:room-12ask— record the question and notify the expert where a frontend is listening.textQuestion `q-7b41c2e0` put to alice. Notified on their channel: alice.Nobody reachable is not a failure: the question is on their pending list either way, and the reply says so.
pending— the open questions, which is how an expert finds theirs.text2 open question(s): - `q-7b41c2e0` from carol: What is the warranty period for the X200? (asked because no verified citation)answer— record the answer. Under actor confinement only an expert the question was routed to may file one, because the answer is published under their name.
The desk is shared, deliberately
Both the question ledger and the answers live in the shared area, because an answer nobody else can read is a private note rather than knowledge written back. A question put here is visible to everyone who can read that area, so it is not the place for anything private.
This tool stamps no access labels of its own and does not pretend to. Whatever indexes that folder decides who can retrieve the answers — see Access Control and Classification.
With no roster configured, every call answers by saying so rather than failing.